diff --git a/.pylintrc b/.pylintrc index f34cd452bc..fdd9293c40 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,7 +1,7 @@ [MASTER] extension-pkg-allow-list=pydantic ignore=material_library.py, plugins -good-names=ax, im, Lx, Ly, Lz, x0, y0, z0, x, y, z, f, t, y1, y2, x1, x2, xs, ys, zs, Ax, Nx, Ny, Nz, dl, rr, E, H, xx, yy, zz, dx, dy, Jx, Jy, Hx, Hy, dz, e, fp, dt +good-names=ax, im, Lx, Ly, Lz, x0, y0, z0, x, y, z, f, t, y1, y2, x1, x2, xs, ys, zs, Ax, Nx, Ny, Nz, dl, rr, E, H, xx, yy, zz, dx, dy, Jx, Jy, Hx, Hy, dz, e, fp, dt, a, c [BASIC] @@ -10,4 +10,4 @@ max-line-length=100 [pre-commit-hook] command=custom_pylint -disable=pointless-string-statement, too-many-ancestors, too-few-public-methods, fixme, logging-not-lazy, logging-fstring-interpolation, no-self-argument, no-self-use +disable=pointless-string-statement, too-many-ancestors, too-few-public-methods, fixme, logging-not-lazy, logging-fstring-interpolation, no-self-argument, no-self-use, duplicate-code diff --git a/PR.json b/PR.json new file mode 100644 index 0000000000..e473a34b83 --- /dev/null +++ b/PR.json @@ -0,0 +1,21 @@ +{ + "name": null, + "frequency_range": [ + -1e+16, + 1e+16 + ], + "eps_inf": 1.0, + "poles": [ + [ + [ + 1.0, + 1.0 + ], + [ + 0.0, + 2.2 + ] + ] + ], + "type": "PoleResidue" +} \ No newline at end of file diff --git a/README.md b/README.md index df564c9588..42a2e5335b 100644 --- a/README.md +++ b/README.md @@ -199,20 +199,20 @@ git push origin x.x.x - [ ] Near2far with new API (3 days) - [ ] Mode Monitor consistent with new .epsilon() (1 day) - [ ] API changes (discuss first, implementation in 1 day) - - [ ] Freqs and times store start, end, stop / number instead of raw values. - - [ ] Change source polarization to E instead of J. - - [ ] named Meidums? - - [ ] Symmetry, PML, grid spec. Less clunky interface? + - [x] Freqs and times store start, end, stop / number instead of raw values. + - [x] Change source polarization to E instead of J. + - [x] named Meidums? + - [x] Symmetry, PML, grid spec. Less clunky interface? - [ ] Covering features of existing code (1 day) - - [ ] support diagonal anisotropy (permittivity as 3-tuple) + - [x] support diagonal anisotropy (permittivity as 3-tuple) + - [x] Conversion of dispersive materials into pole-residue. + - [x] gaussian beam. + - [x] option to display cell boundaries in plot. - [ ] gds slab / gds importing. - - [ ] Conversion of dispersive materials into pole-residue. - - [ ] gaussian beam. - - [ ] option to display cell boundaries in plot. - - [ ] Add PEC medium + - [x] Add PEC medium - [ ] Documentation (1 week) - - [ ] Add more discussion into Simulation docs. - - [ ] Write docstrings and examples for all callables. + - [x] Add more discussion into Simulation docs. + - [x] Write docstrings and examples for all callables. - [ ] How Do I? - [ ] Developer guide - [ ] Package structure guide / explanation. @@ -222,7 +222,7 @@ git push origin x.x.x - [ ] Add more info / debug logging and more comprehensive error handling (file IO, etc). - [ ] Add more intelligent 'inf' handling. - [ ] setup.cfg for installing dependencies for different parts of the code (base, docs, tests) - - [ ] web.monitor using running status for progress updates. + - [ ] web.monitor using running status for progress updates <- waiting on victor. --- diff --git a/Untitled.ipynb b/Untitled.ipynb new file mode 100644 index 0000000000..e203c71cf8 --- /dev/null +++ b/Untitled.ipynb @@ -0,0 +1,59 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "cd7b162c-38b8-493f-87eb-91e6c263f0fe", + "metadata": {}, + "outputs": [], + "source": [ + "import sys; sys.path.append('.')\n", + "import tidy3d as td\n", + "from tidy3d.components.base import Tidy3dBaseModel" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "443f96c7-ae3a-4f6a-8963-e8e76abf7a76", + "metadata": {}, + "outputs": [], + "source": [ + "class Inf(Tidy3dBaseModel):\n", + "\n", + " self.sign : bool = True\n", + " self.shift : \n", + " def __neg__(self):\n", + " if self.is_positive:\n", + " self.is_positive = False\n", + " else:\n", + " self.is_positive = True\n", + " return self\n", + "\n", + " def __lt__(self, other):\n", + " return " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/api.rst b/docs/api.rst index e8b66147b2..7b82ce8b33 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -4,279 +4,359 @@ API Reference .. currentmodule:: tidy3d -Defining Simulations -==================== -Base Simulation Definition --------------------------- +Simulation +========== .. autosummary:: :toctree: _autosummary/ Simulation +Methods +------- -.. Absorbing Boundaries -.. -------------------- +.. autosummary:: + :toctree: _autosummary/ -.. .. autosummary:: -.. :toctree: _autosummary/ + Simulation.plot + Simulation.plot_eps + Simulation.plot_structures + Simulation.plot_structures_eps + Simulation.plot_sources + Simulation.plot_monitors + Simulation.plot_symmetries + Simulation.plot_pml + Simulation.grid + Simulation.dt + Simulation.tmesh + Simulation.wvl_mat_min + Simulation.frequency_range + Simulation.pml_thicknesses + Simulation.num_pml_layers + Simulation.discretize + Simulation.epsilon + + +Grid +==== -.. PML -.. StablePML -.. Absorber +.. autosummary:: + :toctree: _autosummary/ + Coords + FieldGrid + YeeGrid + Coords1D + Grid + Grid.centers + Grid.sizes + Grid.yee -.. Geometry -.. -------- -.. .. autosummary:: -.. :toctree: _autosummary/ +Absorbing Boundaries +==================== -.. Box -.. Sphere -.. Cylinder -.. PolySlab +.. autosummary:: + :toctree: _autosummary/ + PML + StablePML + Absorber -.. Physical Objects -.. ---------------- +Absorber Parameters +------------------- -.. .. autosummary:: -.. :toctree: _autosummary/ +.. autosummary:: + :toctree: _autosummary/ -.. Structure -.. Medium -.. PoleResidue -.. Sellmeier -.. Debye -.. Lorentz -.. plugins.DispersionFitter -.. .. material_library + AbsorberParams + PMLParams -.. Monitors -.. -------- +Geometry +======== -.. .. autosummary:: -.. :toctree: _autosummary/ +.. autosummary:: + :toctree: _autosummary/ -.. FieldMonitor -.. FieldTimeMonitor -.. FluxMonitor -.. FluxTimeMonitor -.. ModeMonitor -.. Mode + Box + Sphere + Cylinder + PolySlab +Methods +------- -.. Simulation Output Data -.. ---------------------- +.. autosummary:: + :toctree: _autosummary/ -.. .. autosummary:: -.. :toctree: _autosummary/ + Geometry.plot + Geometry.inside + Geometry.intersections + Geometry.intersects + Geometry.intersects_plane + Geometry.bounds + Geometry.bounding_box + Geometry.pop_axis + Geometry.unpop_axis -.. SimulationData -.. SimulationData.export -.. SimulationData.load -.. FieldData -.. FluxData -.. FluxTimeData -.. ModeData +Mediums +======= -.. Submitting Simulations -.. ====================== +.. autosummary:: + :toctree: _autosummary/ -.. .. currentmodule:: tidy3d + Medium + AnisotropicMedium + PEC + PoleResidue + Sellmeier + Debye + Lorentz -.. Web API -.. ------- +Methods +------- -.. .. autosummary:: -.. :toctree: _autosummary/ +.. autosummary:: + :toctree: _autosummary/ -.. web.upload -.. web.get_info -.. web.get_run_info -.. web.run -.. web.monitor -.. web.download -.. web.load_data -.. web.delete + AbstractMedium.eps_model -.. Job Interface -.. ------------- -.. .. autosummary:: -.. :toctree: _autosummary/ +Structures +========== -.. web.Job +.. autosummary:: + :toctree: _autosummary/ -.. Batch Processing -.. ---------------- + Structure -.. .. autosummary:: -.. :toctree: _autosummary/ +Methods +------- -.. web.Batch +.. autosummary:: + :toctree: _autosummary/ -.. Info Containers -.. --------------- + Structure.plot -.. .. autosummary:: -.. :toctree: _autosummary/ -.. web.task.Task -.. web.task.TaskInfo -.. web.task.TaskStatus +Modes +===== +.. autosummary:: + :toctree: _autosummary/ -.. Plugins -.. ======= + Mode -.. Dispersive Model Fitting Tool -.. ----------------------------- -.. .. autosummary:: -.. :toctree: _autosummary/ +Sources +======= -.. plugins.DispersionFitter -.. plugins.DispersionFitter.load -.. plugins.DispersionFitter.fit -.. plugins.DispersionFitter.plot +.. autosummary:: + :toctree: _autosummary/ -.. Mode Solver -.. ----------- + VolumeSource + PlaneWave + ModeSource + GaussianPulse -.. .. autosummary:: -.. :toctree: _autosummary/ +Methods +------- -.. plugins.ModeSolver -.. plugins.ModeSolver.solve -.. plugins.mode.mode_solver.ModeInfo +.. autosummary:: + :toctree: _autosummary/ -.. Near Field to Far Field Transformation -.. -------------------------------------- + Source.geometry + Source.plot + Source.inside + Source.intersections + Source.intersects + Source.intersects_plane + Source.bounds + Source.bounding_box + Source.pop_axis + Source.unpop_axis -.. .. autosummary:: -.. :toctree: _autosummary/ +Source Time Dependence +---------------------- -.. plugins.Near2Far -.. plugins.Near2Far.fields_cartesian -.. plugins.Near2Far.fields_spherical -.. plugins.Near2Far.power_cartesian -.. plugins.Near2Far.power_spherical -.. plugins.Near2Far.radar_cross_section +.. autosummary:: + :toctree: _autosummary/ + GaussianPulse + ContinuousWave + SourceTime.amp_time + SourceTime.plot + SourceTime.frequency_range -.. Simulation -.. ========== -.. .. autosummary:: -.. :toctree: _autosummary/ +Monitors +======== -.. Simulation -.. PMLLayer +.. autosummary:: + :toctree: _autosummary/ + FieldMonitor + FieldTimeMonitor + FluxMonitor + FluxTimeMonitor + ModeMonitor -.. Geometry -.. ======== +Methods +------- -.. .. autosummary:: -.. :toctree: _autosummary/ +.. autosummary:: + :toctree: _autosummary/ -.. Box -.. Sphere -.. Cylinder -.. PolySlab + Monitor.geometry + Monitor.plot + Monitor.inside + Monitor.intersections + Monitor.intersects + Monitor.intersects_plane + Monitor.bounds + Monitor.bounding_box + Monitor.pop_axis + Monitor.unpop_axis -.. Medium -.. ====== -.. .. autosummary:: -.. :toctree: _autosummary/ +Output Data +=========== + +.. autosummary:: + :toctree: _autosummary/ -.. Medium + SimulationData + FieldData + FluxData + FluxTimeData + ModeData -.. Dispersive Media -.. ---------------- -.. .. autosummary:: -.. :toctree: _autosummary/ +Tidy3dBaseModel +=============== -.. PoleResidue -.. Sellmeier -.. Lorentz -.. Debye +.. autosummary:: + :toctree: _autosummary/ -.. Material Library -.. ---------------- + components.base.Tidy3dBaseModel + components.base.Tidy3dBaseModel.export + components.base.Tidy3dBaseModel.load + components.base.Tidy3dBaseModel.help +.. Constants +.. ========= .. .. autosummary:: .. :toctree: _autosummary/ +.. constants -.. material_library +Log +=== +.. autosummary:: + :toctree: _autosummary/ -.. Structure -.. ========= + logging_level -.. .. autosummary:: -.. :toctree: _autosummary/ -.. Structure +Submitting Simulations +====================== + +Web API +------- + +.. autosummary:: + :toctree: _autosummary/ + web.run + web.upload + web.get_info + web.start + web.monitor + web.download + web.load_data + web.delete -.. Source -.. ====== +Job Interface +------------- -.. .. autosummary:: -.. :toctree: _autosummary/ +.. autosummary:: + :toctree: _autosummary/ -.. VolumeSource -.. ModeSource -.. PlaneWave -.. ..GaussianBeam + web.Job + web.Job.run + web.Job.upload + web.Job.get_info + web.Job.start + web.Job.monitor + web.Job.download + web.Job.load_data + web.Job.delete -.. Source Time Dependence -.. ---------------------- +Batch Processing +---------------- -.. .. autosummary:: -.. :toctree: _autosummary/ +.. autosummary:: + :toctree: _autosummary/ -.. GaussianPulse -.. ..CW + web.Batch + web.Batch.run + web.Batch.upload + web.Batch.get_info + web.Batch.start + web.Batch.monitor + web.Batch.download + web.Batch.load_data + web.Batch.delete +Info Containers +--------------- -.. Monitor -.. ======= +.. autosummary:: + :toctree: _autosummary/ -.. .. autosummary:: -.. :toctree: _autosummary/ + web.task.Task + web.task.TaskInfo + web.task.TaskStatus -.. FluxMonitor -.. FieldMonitor -.. ModeMonitor -.. Monitor Samplers -.. ---------------- +Plugins +======= -.. .. autosummary:: -.. :toctree: _autosummary/ +Dispersive Model Fitting Tool +----------------------------- -.. TimeSampler -.. FreqSampler +.. autosummary:: + :toctree: _autosummary/ -.. uniform_times -.. uniform_freqs + plugins.DispersionFitter + plugins.DispersionFitter.load + plugins.DispersionFitter.fit + plugins.DispersionFitter.plot +Mode Solver +----------- -.. Modes -.. ===== +.. autosummary:: + :toctree: _autosummary/ -.. .. autosummary:: -.. :toctree: _autosummary/ + plugins.ModeSolver + plugins.ModeSolver.solve + plugins.mode.mode_solver.ModeInfo + +Near Field to Far Field Transformation +-------------------------------------- + +.. autosummary:: + :toctree: _autosummary/ -.. Mode + plugins.Near2Far + plugins.Near2Far.fields_cartesian + plugins.Near2Far.fields_spherical + plugins.Near2Far.power_cartesian + plugins.Near2Far.power_spherical + plugins.Near2Far.radar_cross_section diff --git a/docs/conf.py b/docs/conf.py index 1e51d752f3..fb966d21c0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -24,8 +24,6 @@ def find_version(*file_paths): raise RuntimeError("Unable to find version string.") -# - # -- Project information ----------------------------------------------------- project = "Tidy3d" @@ -64,7 +62,7 @@ def find_version(*file_paths): autodoc_pydantic_model_show_validator_summary = False autodoc_pydantic_model_show_validator_members = False autodoc_pydantic_model_show_field_summary = False -autodoc_pydantic_model_members = True +autodoc_pydantic_model_members = False extlinks = {} diff --git a/explore/Inf.ipynb b/explore/Inf.ipynb index b47b088ee0..0e9e20a660 100644 --- a/explore/Inf.ipynb +++ b/explore/Inf.ipynb @@ -2,120 +2,181 @@ "cells": [ { "cell_type": "code", - "execution_count": 6, + "execution_count": 1, "id": "b5b474ee-7312-46a7-ad1d-fb7e43c0ec2c", "metadata": {}, "outputs": [], "source": [ "import sys\n", - "sys.path.append('../tidy3d/components')\n", + "sys.path.append('..')\n", "\n", - "from base import Tidy3dBaseModel\n", + "from tidy3d.components.base import Tidy3dBaseModel\n", "from typing import Literal" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 53, "id": "ec27769e-03ee-40b2-b0b9-078243b5e8d3", "metadata": {}, "outputs": [], "source": [ "class Inf(Tidy3dBaseModel):\n", " type: Literal['Inf'] = 'Inf'\n", - " center: float = 0.0\n", - " scale: float = 1.0\n", - " \n", - " def get_value(self, bmin, bmax):\n", - " bcenter = (bmin + bmax) / 2.0\n", - " bsize = abs(bmax - bmin)\n", - " return # something\n", + " value: float = 1 # fictional \"value\" in the inf world, units of np.inf\n", "\n", " def __neg__(self):\n", - " return Inf(center=self.center, scale=-1*self.scale)\n", + " return Inf(value=-self.value)\n", " \n", " def __add__(self, other):\n", - " other = float(other)\n", - " new_center = other + self.center\n", - " return Inf(center=new_center, scale=self.scale)\n", + " if isinstance(other, Inf):\n", + " new_value = self.value + other.value\n", + " # special case, if it's exactly, 0 just return zero\n", + " if new_value == 0.0:\n", + " return 0.0\n", + " return Inf(value=new_value)\n", + " return Inf(value=self.value + other)\n", "\n", " def __sub__(self, other):\n", - " return self.__add__(-1*other)\n", + " return self + -other\n", "\n", " def __mul__(self, other):\n", - " other = float(other)\n", - " new_scale = self.scale * other\n", - " return Inf(center=self.center, scale=new_scale)\n", + " if isinstance(other, Inf):\n", + " new_value = self.value * other.value\n", + " # special case, if it's exactly, 0 just return zero\n", + " if new_value == 0.0:\n", + " return 0.0 \n", + " return Inf(value=new_value)\n", + " return Inf(value=self.value * other)\n", "\n", " def __div__(self, other):\n", - " return self.__mul__(1/other) " + " return self.__mul__(1.0 / other)\n", + "\n", + " def __truediv__(self, other):\n", + " return self.__mul__(1.0 / other)\n", + "\n", + " def __eq__(self, other):\n", + " if isinstance(other, Inf):\n", + " return self.value == other.value\n", + " return False\n", + "\n", + " def __lt__(self, other):\n", + " if isinstance(other, Inf):\n", + " return self.value < other.value\n", + " return True if self.value < 0 else False\n", + "\n", + " def __gt__(self, other):\n", + " if isinstance(other, Inf):\n", + " return self.value > other.value\n", + " return True if self.value > 0 else False\n", + "\n", + " def __le__(self, other):\n", + " if isinstance(other, Inf):\n", + " return self.value < other.value\n", + " return True if self.value < 0 else False\n", + "\n", + " def __ge__(self, other):\n", + " if isinstance(other, Inf):\n", + " return self.value >= other.value\n", + " return True if self.value >= 0 else False" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 54, "id": "91058320-429c-49b2-9908-bce983a113a7", "metadata": {}, + "outputs": [], + "source": [ + "inf = Inf()" + ] + }, + { + "cell_type": "code", + "execution_count": 61, + "id": "6957c5d9-617f-462f-a7f7-6eb43d01547c", + "metadata": {}, + "outputs": [], + "source": [ + "# properties to support\n", + "\n", + "# can be negative\n", + "assert -inf < inf\n", + "\n", + "# is larger (smaller) than any number\n", + "assert inf > 1e122\n", + "assert -inf < -1e122\n", + "\n", + "# can be compared to versions of itself modified with basic algebra\n", + "assert inf < inf*2\n", + "assert inf < inf + 1.0\n", + "assert inf > inf/2\n", + "assert inf > inf - 1.0\n", + "assert -inf > -inf*2\n", + "assert -inf < -inf + 1.0\n", + "assert -inf < -inf/2\n", + "assert -inf > -inf - 1.0\n", + "\n", + "# algebra between two infs leads to values that are expected intuitively\n", + "assert inf/2 - inf/2 == 0.0\n", + "assert -inf/3 + inf/3 == 0.0\n", + "assert inf/2 + inf/3 < inf\n" + ] + }, + { + "cell_type": "code", + "execution_count": 62, + "id": "8f0b6360-314d-4d8a-9b49-eefefd03ce62", + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "{\"type\": \"Inf\", \"center\": 0.0, \"scale\": 1.0}\n" + "type='Inf' value=1.5\n" ] } ], "source": [ - "inf = Inf()\n", - "print(inf.json())" + "print(inf+inf/2)" ] }, { "cell_type": "code", - "execution_count": 12, - "id": "6957c5d9-617f-462f-a7f7-6eb43d01547c", + "execution_count": 63, + "id": "a64c4a0f-f003-4942-bb6e-2a882ce51008", "metadata": {}, "outputs": [ { "data": { + "text/html": [ + "
0.5\n",
+       "
\n" + ], "text/plain": [ - "'{\"type\": \"Inf\", \"center\": 0.0, \"scale\": -1.0}'" + "\u001b[1;36m0.5\u001b[0m\n" ] }, - "execution_count": 12, "metadata": {}, - "output_type": "execute_result" + "output_type": "display_data" } ], "source": [ - "(-inf).json()" + "center.value" ] }, { "cell_type": "code", - "execution_count": 13, - "id": "4c249540-60be-448d-a66f-82a192e7f523", + "execution_count": null, + "id": "e99b2fe0-8b83-409f-a046-1d11d4daf9e8", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'{\"type\": \"Inf\", \"center\": 1.0, \"scale\": -1.0}'" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "(-inf+1.0).json()" - ] + "outputs": [], + "source": [] }, { "cell_type": "code", "execution_count": null, - "id": "791d3495-04b2-467a-9a15-26d98a378cca", + "id": "2e5eebe5-55fc-4079-8921-6b580782de60", "metadata": {}, "outputs": [], "source": [] diff --git a/test_all.sh b/test_all.sh index f2348f03bc..5ea6c282fa 100755 --- a/test_all.sh +++ b/test_all.sh @@ -3,4 +3,8 @@ black . python lint.py pytest -rA tests/ -pytest --doctest-modules tidy3d --ignore=tidy3d/__main__.py \ No newline at end of file +pytest --doctest-modules tidy3d \ +--ignore=tidy3d/__main__.py \ +--ignore=tidy3d/components/base.py \ +--ignore=tidy3d/web/webapi.py \ +--ignore=tidy3d/web/container.py \ \ No newline at end of file diff --git a/test_static.sh b/test_static.sh index caabd608f6..0c82c78d0c 100755 --- a/test_static.sh +++ b/test_static.sh @@ -8,4 +8,8 @@ pytest -rA tests/test_material_library.py pytest -rA tests/test_core.py pytest -rA tests/test_plugins.py -pytest --doctest-modules tidy3d --ignore=tidy3d/__main__.py +pytest --doctest-modules tidy3d \ +--ignore=tidy3d/__main__.py \ +--ignore=tidy3d/components/base.py \ +--ignore=tidy3d/web/webapi.py \ +--ignore=tidy3d/web/container.py \ diff --git a/tests/test_IO.py b/tests/test_IO.py index a18ebc2082..90f90c63bc 100644 --- a/tests/test_IO.py +++ b/tests/test_IO.py @@ -36,7 +36,7 @@ def test_simulation_preserve_types(): geometry=PolySlab(vertices=[[0, 0], [2, 3], [4, 3]], slab_bounds=(-1, 1), axis=2), medium=Sellmeier(coeffs=[]), ), - Structure(geometry=Sphere(radius=1), medium=Debye(eps_inf=1.0, coeffs=[])), + Structure(geometry=Sphere(radius=1), medium=Debye(eps_inf=1.0, coeffs=[]), name="t2"), ], sources=[ VolumeSource(size=(0, 0, 0), source_time=st, polarization="Ex"), @@ -46,7 +46,7 @@ def test_simulation_preserve_types(): source_time=st, direction="+", polarization="Ex", - waist_size=(1, 1), + waist_radius=1, ), ], monitors=[ @@ -101,7 +101,12 @@ def test_validation_speed(): for n in num_structures: S = SIM.copy() - S.structures = n * [SIM.structures[0]] + new_structures = [] + for i in range(n): + new_structure = SIM.structures[0].copy() + new_structure.name = str(i) + new_structures.append(new_structure) + S.structures = new_structures S.export(path) time_start = time() diff --git a/tests/test_components.py b/tests/test_components.py index 881da938f8..dafb5e95e5 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -207,10 +207,15 @@ def test_medium_conversions(): assert np.isclose(k, k_) +def test_PEC(): + + struct = Structure(geometry=Box(size=(1, 1, 1)), medium=PEC) + + def test_medium_dispersion(): # construct media - m_PR = PoleResidue(eps_inf=1.0, poles=[((1, 2), (1, 3)), ((2, 4), (1, 5))]) + m_PR = PoleResidue(eps_inf=1.0, poles=[(1 + 2j, 1 + 3j), (2 + 4j, 1 + 5j)]) m_SM = Sellmeier(coeffs=[(2, 3), (2, 4)]) m_LZ = Lorentz(eps_inf=1.0, coeffs=[(1, 3, 2), (2, 4, 1)]) m_DB = Debye(eps_inf=1.0, coeffs=[(1, 3), (2, 4)]) @@ -220,6 +225,153 @@ def test_medium_dispersion(): eps_c = medium.eps_model(freqs) +def test_medium_dispersion_conversion(): + + m_PR = PoleResidue(eps_inf=1.0, poles=[((1 + 2j), (1 + 3j)), ((2 + 4j), (1 + 5j))]) + m_SM = Sellmeier(coeffs=[(2, 3), (2, 4)]) + m_LZ = Lorentz(eps_inf=1.0, coeffs=[(1, 3, 2), (2, 4, 1)]) + m_DB = Debye(eps_inf=1.0, coeffs=[(1, 3), (2, 4)]) + + freqs = np.linspace(0.01, 1, 1001) + for medium in [m_PR, m_SM, m_DB, m_LZ]: # , m_DB]: + eps_model = medium.eps_model(freqs) + eps_pr = medium.pole_residue.eps_model(freqs) + np.testing.assert_allclose(eps_model, eps_pr) + + +""" modes """ + + +def test_modes(): + + m = Mode(mode_index=0) + m = Mode(mode_index=0, num_modes=1) + + # not enough modes + with pytest.raises(SetupError) as e: + m = Mode(mode_index=1, num_modes=1) + + +""" names """ + + +def test_names_default(): + """makes sure default names are set""" + + sim = Simulation( + size=(2.0, 2.0, 2.0), + grid_size=(0.01, 0.01, 0.01), + run_time=1e-12, + structures=[ + Structure( + geometry=Box(size=(1, 1, 1), center=(-1, 0, 0)), + medium=Medium(permittivity=2.0), + ), + Structure( + geometry=Box(size=(1, 1, 1), center=(0, 0, 0)), + medium=Medium(permittivity=2.0), + ), + Structure(geometry=Sphere(radius=1.4, center=(1.0, 0.0, 1.0)), medium=Medium()), + Structure( + geometry=Cylinder(radius=1.4, length=2.0, center=(1.0, 0.0, -1.0), axis=1), + medium=Medium(), + ), + ], + sources=[ + VolumeSource( + size=(0, 0, 0), + center=(0, -0.5, 0), + polarization="Hx", + source_time=GaussianPulse(freq0=1e14, fwidth=1e12), + ), + VolumeSource( + size=(0, 0, 0), + center=(0, -0.5, 0), + polarization="Ex", + source_time=GaussianPulse(freq0=1e14, fwidth=1e12), + ), + VolumeSource( + size=(0, 0, 0), + center=(0, -0.5, 0), + polarization="Ey", + source_time=GaussianPulse(freq0=1e14, fwidth=1e12), + ), + ], + monitors=[ + FluxMonitor(size=(1, 1, 0), center=(0, -0.5, 0), freqs=[1], name="mon1"), + FluxMonitor(size=(0, 1, 1), center=(0, -0.5, 0), freqs=[1], name="mon2"), + FluxMonitor(size=(1, 0, 1), center=(0, -0.5, 0), freqs=[1], name="mon3"), + ], + ) + + for i, structure in enumerate(sim.structures): + assert structure.name == f"structures[{i}]" + + for i, source in enumerate(sim.sources): + assert source.name == f"sources[{i}]" + + distinct_mediums = [f"mediums[{i}]" for i in range(len(sim.mediums))] + for i, medium in enumerate(sim.mediums): + assert medium.name in distinct_mediums + distinct_mediums.pop(distinct_mediums.index(medium.name)) + + +def test_names_unique(): + + with pytest.raises(SetupError) as e: + sim = Simulation( + size=(2.0, 2.0, 2.0), + grid_size=(0.01, 0.01, 0.01), + run_time=1e-12, + structures=[ + Structure( + geometry=Box(size=(1, 1, 1), center=(-1, 0, 0)), + medium=Medium(permittivity=2.0), + name="struct1", + ), + Structure( + geometry=Box(size=(1, 1, 1), center=(0, 0, 0)), + medium=Medium(permittivity=2.0), + name="struct1", + ), + ], + ) + + with pytest.raises(SetupError) as e: + sim = Simulation( + size=(2.0, 2.0, 2.0), + grid_size=(0.01, 0.01, 0.01), + run_time=1e-12, + sources=[ + VolumeSource( + size=(0, 0, 0), + center=(0, -0.5, 0), + polarization="Hx", + source_time=GaussianPulse(freq0=1e14, fwidth=1e12), + name="source1", + ), + VolumeSource( + size=(0, 0, 0), + center=(0, -0.5, 0), + polarization="Ex", + source_time=GaussianPulse(freq0=1e14, fwidth=1e12), + name="source1", + ), + ], + ) + + with pytest.raises(SetupError) as e: + sim = Simulation( + size=(2.0, 2.0, 2.0), + grid_size=(0.01, 0.01, 0.01), + run_time=1e-12, + monitors=[ + FluxMonitor(size=(1, 1, 0), center=(0, -0.5, 0), freqs=[1], name="mon1"), + FluxMonitor(size=(0, 1, 1), center=(0, -0.5, 0), freqs=[1], name="mon1"), + ], + ) + + """ VolumeSources """ diff --git a/tests/test_grid.py b/tests/test_grid.py index 978045675c..65362139d2 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -33,7 +33,7 @@ def test_grid(): assert np.all(g.centers.y == np.array([-1.5, -0.5, 0.5, 1.5])) assert np.all(g.centers.z == np.array([-2.5, -1.5, -0.5, 0.5, 1.5, 2.5])) - for s in g.cell_sizes.dict().values(): + for s in g.sizes.dict().values(): assert np.all(s == 1.0) assert np.all(g.yee.E.x.x == np.array([-0.5, 0.5])) diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 38ebaa8f9c..1107dcae77 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -4,20 +4,41 @@ from rich import pretty, traceback # import component as `from tidy3d import Simulation` or `td.Simulation` + +# pml from .components import PML, StablePML, Absorber +from .components import PMLParams, AbsorberParams +from .components import DefaultPMLParameters, DefaultStablePMLParameters, DefaultAbsorberParameters + +# grid from .components import Grid, Coords + +# geometry from .components import Box, Sphere, Cylinder, PolySlab -from .components import Geometry -from .components import Structure -from .components import Medium, PoleResidue, Sellmeier, Debye, Lorentz + +# medium +from .components import Medium, PoleResidue, Sellmeier, Debye, Lorentz, AnisotropicMedium, PEC from .components import nk_to_eps_complex, nk_to_eps_sigma, eps_complex_to_nk from .components import nk_to_medium, eps_sigma_to_eps_complex -from .components import GaussianPulse + +# structures +from .components import Structure + +# modes +from .components import Mode + +# sources +from .components import GaussianPulse, ContinuousWave from .components import VolumeSource, PlaneWave, ModeSource, GaussianBeam + +# monitors from .components import FieldMonitor, FieldTimeMonitor, FluxMonitor, FluxTimeMonitor from .components import ModeMonitor -from .components import Mode + +# simulation from .components import Simulation + +# data from .components import SimulationData, FieldData, FluxData, ModeData, FluxTimeData from .components import data_type_map, ScalarFieldData, ScalarFieldTimeData @@ -31,7 +52,15 @@ # get material `mat` and variant `var` as `material_library[mat][var]` from .material_library import material_library -from .log import log +# logging +from .log import log, logging_level + +# for docs +from .components.medium import AbstractMedium +from .components.geometry import Geometry +from .components.source import Source, SourceTime +from .components.monitor import Monitor +from .components.grid import YeeGrid, FieldGrid, Coords1D # make all stdout and errors pretty pretty.install() diff --git a/tidy3d/components/__init__.py b/tidy3d/components/__init__.py index 4f1349014c..420c7d68b0 100644 --- a/tidy3d/components/__init__.py +++ b/tidy3d/components/__init__.py @@ -1,24 +1,39 @@ """ Imports all tidy3d """ -from .simulation import Simulation - +# pml from .pml import PML, StablePML, Absorber +from .pml import PMLParams, AbsorberParams +from .pml import DefaultPMLParameters, DefaultStablePMLParameters, DefaultAbsorberParameters + +# grid from .grid import Grid, Coords +# geometry from .geometry import Box, Sphere, Cylinder, PolySlab from .geometry import Geometry -from .structure import Structure -from .medium import Medium, PoleResidue, Sellmeier, Debye, Lorentz + +# medium +from .medium import Medium, PoleResidue, Sellmeier, Debye, Lorentz, AnisotropicMedium, PEC from .medium import nk_to_eps_complex, nk_to_eps_sigma, eps_complex_to_nk from .medium import nk_to_medium, eps_sigma_to_eps_complex -from .source import GaussianPulse +# structure +from .structure import Structure + +# mode +from .mode import Mode + +# source +from .source import GaussianPulse, ContinuousWave from .source import VolumeSource, PlaneWave, ModeSource, GaussianBeam +# monitor from .monitor import FieldMonitor, FieldTimeMonitor, FluxMonitor, FluxTimeMonitor from .monitor import ModeMonitor -from .mode import Mode +# simulation +from .simulation import Simulation +# data from .data import SimulationData, FieldData, FluxData, ModeData, FluxTimeData from .data import ScalarFieldData, ScalarFieldTimeData, data_type_map diff --git a/tidy3d/components/base.py b/tidy3d/components/base.py index 31cee8623c..5619c0c631 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -1,4 +1,4 @@ -""" global configuration / base class for pydantic models used to make simulation """ +"""global configuration / base class for pydantic models used to make simulation.""" import json @@ -7,61 +7,213 @@ import yaml import numpy as np +from .types import ComplexNumber +from ..log import FileError + # default indentation (# spaces) in files INDENT = 4 class Tidy3dBaseModel(pydantic.BaseModel): - """https://pydantic-docs.helpmanual.io/usage/model_config/""" + """Base pydantic model that all Tidy3d components inherit from. + Defines configuration for handling data structures + as well as methods for imporing, exporting, and hashing tidy3d objects. + For more details on pydantic base models, see: + `Pydantic Models `_ + """ class Config: # pylint: disable=too-few-public-methods - """sets config for all Tidy3dBaseModel objects""" + """Sets config for all :class:`Tidy3dBaseModel` objects. + + Configuration Options + --------------------- + allow_population_by_field_name : bool = True + Allow properties to stand in for fields(?). + arbitrary_types_allowed : bool = True + Allow types like numpy arrays. + extra : str = 'forbid' + Forbid extra kwargs not specified in model. + json_encoders : Dict[type, Callable] + Defines how to encode type in json file. + validate_all : bool = True + Validate default values just to be safe. + validate_assignment : bool + Re-validate after re-assignment of field in model. + """ arbitrary_types_allowed = True - validate_all = True # validate default values too - extra = "forbid" # forbid extra kwargs not specified in model - validate_assignment = True # validate when attributes are set after initialization + validate_all = True + extra = "forbid" + validate_assignment = True allow_population_by_field_name = True - json_encoders = {np.ndarray: lambda x: x.tolist()} # pylint: disable=unnecessary-lambda + json_encoders = { + np.ndarray: lambda x: x.tolist(), + complex: lambda x: ComplexNumber(real=np.real(x), imag=np.imag(x)), + } + json_decoders = {ComplexNumber: lambda x: x.real + 1j * x.imag} def help(self, methods: bool = False) -> None: - """get help for this object""" + """Prints message describing the fields and methods of a :class:`Tidy3dBaseModel`. + + Parameters + ---------- + methods : bool = False + Whether to also print out information about object's methods. + + Example + ------- + >>> simulation.help(methods=True) + """ rich.inspect(self, methods=methods) - def __hash__(self) -> int: - """hash tidy3dBaseModel objects using their json strings""" - return hash(self.json()) + @classmethod + def load(cls, fname: str): + """Loads a :class:`Tidy3dBaseModel` from .yaml or .json file. - def __lt__(self, other): - """define < for getting unique indices""" - return hash(self) < hash(other) + Parameters + ---------- + fname : str + Full path to the .yaml or .json file to load the :class:`Tidy3dBaseModel` from. - def _json_string(self, exclude_unset: bool = False) -> str: - """returns string representation of self""" - return self.json(indent=INDENT, exclude_unset=exclude_unset) + Returns + ------- + :class:`Tidy3dBaseModel` + An instance of the component class calling `load`. + + Example + ------- + >>> simulation = Simulation.load(fname='folder/sim.json') + """ + if ".json" in fname: + return cls.load_json(fname=fname) + if ".yaml" in fname: + return cls.load_yaml(fname=fname) + raise FileError(f"File must be .json or .yaml, given {fname}") + + def export(self, fname: str) -> None: + """Exports :class:`Tidy3dBaseModel` instance to .yaml or .json file + + Parameters + ---------- + fname : str + Full path to the .yaml or .json file to save the :class:`Tidy3dBaseModel` to. + + Example + ------- + >>> simulation.export(fname='folder/sim.json') + """ + if ".json" in fname: + return self.export_json(fname=fname) + if ".yaml" in fname: + return self.export_yaml(fname=fname) + raise FileError(f"File must be .json or .yaml, given {fname}") @classmethod - def load(cls, fname: str): - """load Simulation from .json file""" + def load_json(cls, fname: str): + """Load a :class:`Tidy3dBaseModel` from .json file. + + Parameters + ---------- + fname : str + Full path to the .json file to load the :class:`Tidy3dBaseModel` from. + + Returns + ------- + :class:`Tidy3dBaseModel` + An instance of the component class calling `load`. + + Example + ------- + >>> simulation = Simulation.load_json(fname='folder/sim.json') + """ return cls.parse_file(fname) - def export(self, fname: str) -> None: - """Exports Tidy3dBaseModel instance to .json file""" + def export_json(self, fname: str) -> None: + """Exports :class:`Tidy3dBaseModel` instance to .json file + + Parameters + ---------- + fname : str + Full path to the .json file to save the :class:`Tidy3dBaseModel` to. + + Example + ------- + >>> simulation.export_json(fname='folder/sim.json') + """ json_string = self._json_string() with open(fname, "w", encoding="utf-8") as file_handle: file_handle.write(json_string) @classmethod def load_yaml(cls, fname: str): - """load Simulation from .yaml file""" + """Loads :class:`Tidy3dBaseModel` from .yaml file. + + Parameters + ---------- + fname : str + Full path to the .yaml file to load the :class:`Tidy3dBaseModel` from. + + Returns + ------- + :class:`Tidy3dBaseModel` + An instance of the component class calling `load_yaml`. + + Example + ------- + >>> simulation = Simulation.load_yaml(fname='folder/sim.yaml') + """ with open(fname, "r", encoding="utf-8") as yaml_in: json_dict = yaml.safe_load(yaml_in) json_raw = json.dumps(json_dict, indent=INDENT) return cls.parse_raw(json_raw) def export_yaml(self, fname: str) -> None: - """Exports Tidy3dBaseModel instance to .yaml file""" + """Exports :class:`Tidy3dBaseModel` instance to .yaml file. + + Parameters + ---------- + fname : str + Full path to the .yaml file to save the :class:`Tidy3dBaseModel` to. + + Example + ------- + >>> simulation.export_yaml(fname='folder/sim.yaml') + """ json_string = self._json_string() json_dict = json.loads(json_string) with open(fname, "w+", encoding="utf-8") as file_handle: yaml.dump(json_dict, file_handle, indent=INDENT) + + def __hash__(self) -> int: + """Hash a :class:`Tidy3dBaseModel` objects using its json string. + + Returns + ------- + int + Integer representation of the hash of the :class:`Tidy3dBaseModel`. + + Example + ------- + >>> hash_integer = hash(simulation) + """ + return hash(self.json()) + + def __lt__(self, other): + """define < for getting unique indices based on hash.""" + return hash(self) < hash(other) + + def _json_string(self, include_unset: bool = True) -> str: + """Returns string representation of a :class:`Tidy3dBaseModel`. + + Parameters + ---------- + include_unset : bool = True + Whether to include default fields in json string. + + Returns + ------- + str + Json-formatted string holding :class:`Tidy3dBaseModel` data. + """ + exclude_unset = not include_unset + return self.json(indent=INDENT, exclude_unset=exclude_unset) diff --git a/tidy3d/components/data.py b/tidy3d/components/data.py index 84e2ec2b0f..8b21caf182 100644 --- a/tidy3d/components/data.py +++ b/tidy3d/components/data.py @@ -20,19 +20,19 @@ def save_string(hdf5_grp, string_key: str, string_value: str) -> None: - """save a string to an hdf5 group""" + """Save a string to an hdf5 group.""" str_type = h5py.special_dtype(vlen=str) hdf5_grp.create_dataset(string_key, (1,), dtype=str_type) hdf5_grp[string_key][0] = string_value def decode_bytes(bytes_dataset) -> str: - """decode an hdf5 dataset containing bytes to a string""" + """Decode an hdf5 dataset containing bytes to a string.""" return bytes_dataset[0].decode("utf-8") def load_string(hdf5_grp, string_key: str) -> str: - """load a string from an hdf5 group""" + """Load a string from an hdf5 group.""" string_value_bytes = hdf5_grp.get(string_key) if not string_value_bytes: return None @@ -40,7 +40,7 @@ def load_string(hdf5_grp, string_key: str) -> str: def decode_bytes_array(array_of_bytes: Numpy) -> List[str]: - """convert numpy array containing bytes to list of strings""" + """Convert numpy array containing bytes to list of strings.""" list_of_bytes = array_of_bytes.tolist() list_of_str = [v.decode("utf-8") for v in list_of_bytes] return list_of_str @@ -50,10 +50,10 @@ def decode_bytes_array(array_of_bytes: Numpy) -> List[str]: class Tidy3dData(Tidy3dBaseModel): - """base class for data associated with a simulation.""" + """Base class for data associated with a simulation.""" class Config: # pylint: disable=too-few-public-methods - """sets config for all Tidy3dBaseModel objects""" + """Configuration for all Tidy3dData objects.""" validate_all = True # validate default values too extra = "allow" # allow extra kwargs not specified in model (like dir=['+', '-']) @@ -68,22 +68,19 @@ class Config: # pylint: disable=too-few-public-methods @abstractmethod def add_to_group(self, hdf5_grp): - """add data contents to an hdf5 group""" + """Add data contents to an hdf5 group.""" @classmethod @abstractmethod def load_from_group(cls, hdf5_grp): - """add data contents to an hdf5 group""" + """Load data contents from an hdf5 group.""" class MonitorData(Tidy3dData, ABC): - """Abstract base class. Stores data. + """Abstract base class for objects storing individual data from simulation.""" - Attributes - ---------- - data : ``Union[xarray.DataArray xarray.Dataset]`` - Representation of the data as an xarray object. - """ + values: Union[Array[float], Array[complex]] + type: str = None """ explanation of values `values` is a numpy array that stores the raw data associated with each @@ -96,8 +93,7 @@ class MonitorData(Tidy3dData, ABC): :class:`MonitorData` subclass """ - values: Union[Array[float], Array[complex]] - type: str = None + _dims = () """ explanation of``_dims`` `_dims` is an attribute of all `MonitorData` objects. @@ -107,23 +103,24 @@ class MonitorData(Tidy3dData, ABC): The dims are used to construct xarray objects as it tells the _make_xarray method what attribute to use for the keys in the `coords` coordinate dictionary. """ - _dims = () @property def data(self) -> xr.DataArray: - """make xarray representation of data + """Returns an xarray representation of the montitor data. Returns ------- - ``xarray.DataArray`` - Representation of the underlying data using xarray. + xarray.DataArray + Representation of the monitor data using xarray. + For more details refer to `xarray's Documentaton `_. """ + data_dict = self.dict() coords = {dim: data_dict[dim] for dim in self._dims} return xr.DataArray(self.values, coords=coords) def __eq__(self, other) -> bool: - """check equality against another MonitorData instance + """Check equality against another MonitorData instance. Parameters ---------- @@ -132,14 +129,14 @@ def __eq__(self, other) -> bool: Returns ------- - ``bool`` + bool Whether the other :class:`MonitorData` instance has the same data. """ assert isinstance(other, MonitorData), "can only check eqality on two monitor data objects" return np.all(self.values == self.values) def add_to_group(self, hdf5_grp) -> None: - """add data contents to an hdf5 group""" + """Add data contents to an hdf5 group.""" # save the type information of MonitorData to the group save_string(hdf5_grp, "type", self.type) @@ -151,7 +148,7 @@ def add_to_group(self, hdf5_grp) -> None: @classmethod def load_from_group(cls, hdf5_grp): - """load the solver data dict for a specific monitor into a MonitorData instance""" + """Load Monitor data instance from an hdf5 group.""" # kwargs that gets passed to MonitorData.__init__() to make new MonitorData kwargs = {} @@ -177,12 +174,12 @@ def load_from_group(cls, hdf5_grp): class CollectionData(Tidy3dData): - """Abstract base class. Stores collection of data with similar dimensions. + """Abstract base class. Stores a collection of data with same dimension types (such as field). Parameters ---------- - data_dict : ``{str : :class:`MonitorData`} - mapping of field name to corresponding :class:`MonitorData`. + data_dict : Dict[str, :class:`MonitorData`] + Mapping of collection member name to corresponding :class:`MonitorData`. """ data_dict: Dict[str, MonitorData] @@ -195,8 +192,9 @@ def data(self) -> xr.Dataset: Returns ------- - ```xarray.Dataset `__`` + xarray.Dataset Representation of the underlying data using xarray. + For more details refer to `xarray's Documentaton `_. """ data_arrays = {name: arr.data for name, arr in self.data_dict.items()} @@ -204,7 +202,7 @@ def data(self) -> xr.Dataset: return xr.Dataset(data_arrays) def __eq__(self, other): - """check for equality against other :class:`CollectionData` object.""" + """Check for equality against other :class:`CollectionData` object.""" # same keys? if not all(k in other.data_dict.keys() for k in self.data_dict.keys()): @@ -218,7 +216,7 @@ def __eq__(self, other): return True def add_to_group(self, hdf5_grp) -> None: - """add data from a :class:`CollectionData` to an hdf5 group .""" + """Add data from a :class:`CollectionData` to an hdf5 group .""" # put collection's type information into the group save_string(hdf5_grp, "type", self.type) @@ -230,7 +228,7 @@ def add_to_group(self, hdf5_grp) -> None: @classmethod def load_from_group(cls, hdf5_grp): - """load a :class:`CollectionData` from hdf5 group containing data.""" + """Load a :class:`CollectionData` from hdf5 group containing data.""" data_dict = {} for data_name, data_value in hdf5_grp.items(): @@ -245,68 +243,55 @@ def load_from_group(cls, hdf5_grp): return cls(data_dict=data_dict) -""" The following -are abstract classes that separate the :class:`MonitorData` instances into - different types depending on what they store. - They can be useful for keeping argument types and validations separated. - For example, monitors that should always be defined on planar geometries can have an - ``_assert_plane()`` validation in the abstract base class ``PlanarData``. - This way, ``_assert_plane()`` will always be used if we add more ``PlanarData`` objects in - the future. - This organization is also useful when doing conditions based on monitor / data type. - For example, instead of - ``if isinstance(mon_data, (FieldData, FieldTimeData)):`` we can simply do - ``if isinstance(mon_data, AbstractFieldData)`` and this will generalize if we add more - ``AbstractFieldData`` objects in the future. -""" +""" Classes of Monitor Data """ class FreqData(MonitorData, ABC): - """Stores frequency-domain data using an ``f`` attribute for frequency (Hz).""" + """Stores frequency-domain data using an ``f`` dimension for frequency in Hz.""" f: Array[float] class TimeData(MonitorData, ABC): - """Stores time-domain data using a ``t`` attribute for time (sec).""" + """Stores time-domain data using a ``t`` attribute for time in seconds.""" t: Array[float] class AbstractScalarFieldData(MonitorData, ABC): - """Stores a single field as a functio of x,y,z and sampler""" + """Stores a single, scalar field as a function of spatial coordinates x,y,z.""" x: Array[float] y: Array[float] z: Array[float] - values: Union[Array[complex], Array[float]] + # values: Union[Array[complex], Array[float]] class PlanarData(MonitorData, ABC): - """Stores data that is constrained to the plane.""" + """Stores data that must be found via a planar monitor.""" class AbstractFluxData(PlanarData, ABC): - """Stores electromagnetic flux through a planar :class:`Monitor`""" + """Stores electromagnetic flux through a plane.""" """ usable monitors """ class ScalarFieldData(AbstractScalarFieldData, FreqData): - """stores a single scalar field in frequency-domain + """Stores a single scalar field in frequency-domain. Parameters ---------- - x : ``numpy.ndarray`` + x : numpy.ndarray Data coordinates in x direction (um). - y : ``numpy.ndarray`` + y : numpy.ndarray Data coordinates in y direction (um). - z : ``numpy.ndarray`` + z : numpy.ndarray Data coordinates in z direction (um). - f : ``numpy.ndarray`` + f : numpy.ndarray Frequency coordinates (Hz). - values : ``numpy.ndarray`` + values : numpy.ndarray Complex-valued array of shape ``(len(x), len(y), len(z), len(f))`` storing field values. Example @@ -330,15 +315,15 @@ class ScalarFieldTimeData(AbstractScalarFieldData, TimeData): Parameters ---------- - x : ``numpy.ndarray`` + x : numpy.ndarray Data coordinates in x direction (um). - y : ``numpy.ndarray`` + y : numpy.ndarray Data coordinates in y direction (um). - z : ``numpy.ndarray`` + z : numpy.ndarray Data coordinates in z direction (um). - t : ``numpy.ndarray`` + t : numpy.ndarray Time coordinates (sec). - values : ``numpy.ndarray`` + values : numpy.ndarray Real-valued array of shape ``(len(x), len(y), len(z), len(t))`` storing field values. Example @@ -358,11 +343,12 @@ class ScalarFieldTimeData(AbstractScalarFieldData, TimeData): class FieldData(CollectionData): - """Stores a collectio of scalar field quantities as a function of x, y, and z. + """Stores a collection of scalar fields + from a :class:`FieldMonitor` or :class:`FieldTimeMonitor`. Parameters ---------- - data_dict : ``{str : Union[:class:`ScalarFieldData`, :class:`ScalarFieldTimeData`]} + data_dict : Dict[str, :class:`ScalarFieldData`] or Dict[str, :class:`ScalarFieldTimeData`] Mapping of field name to its scalar field data. Example @@ -380,18 +366,18 @@ class FieldData(CollectionData): >>> data_t = FieldData(data_dict={'Ex': field_t, 'Ey': field_t}) """ - data_dict: Dict[str, Union[ScalarFieldData, ScalarFieldTimeData]] + data_dict: Union[Dict[str, ScalarFieldData], Dict[str, ScalarFieldTimeData]] type: Literal["FieldData"] = "FieldData" class FluxData(AbstractFluxData, FreqData): - """Stores power flux data through a planar :class:`FluxMonitor`. + """Stores frequency-domain power flux data from a :class:`FluxMonitor`. Parameters ---------- - f : ``numpy.ndarray`` + f : numpy.ndarray Frequency coordinates (Hz). - values : ``numpy.ndarray`` + values : numpy.ndarray Complex-valued array of shape ``(len(f),)`` storing field values. Example @@ -409,13 +395,13 @@ class FluxData(AbstractFluxData, FreqData): class FluxTimeData(AbstractFluxData, TimeData): - """Stores power flux data through a planar :class:`FluxTimeMonitor` + """Stores time-domain power flux data from a :class:`FluxTimeMonitor`. Parameters ---------- - t : ``numpy.ndarray`` + t : numpy.ndarray Time coordinates (sec). - values : ``numpy.ndarray`` + values : numpy.ndarray Real-valued array of shape ``(len(t),)`` storing field values. Example @@ -437,15 +423,16 @@ class ModeData(PlanarData, FreqData): Parameters ---------- - direction : ``[Literal["+", "-"]]`` + direction : List[str] List of strings corresponding to the mode propagation direction. - mode_index : ``numpy.ndarray`` + Allowed elements are ``'+'`` and ``'-'``. + mode_index : numpy.ndarray Array of integer indices into the original monitor's :attr:`ModeMonitor.modes`. - f : ``numpy.ndarray`` + f : numpy.ndarray Frequency coordinates (Hz). - values : ``numpy.ndarray`` + values : numpy.ndarray Complex-valued array of mode amplitude values - with shape``values.shape=(len(direction), len(mode_index), len(f))`` + with shape ``values.shape=(len(direction), len(mode_index), len(f))``. Example ------- @@ -481,11 +468,11 @@ class SimulationData(Tidy3dBaseModel): Parameters ---------- simulation : :class:`Simulation` - Original :class:`Simulation`. - monitor_data : ``Dict[str, :class:`Tidy3dData`]`` + Original :class:`Simulation` that was run to create data. + monitor_data : Dict[str, :class:`Tidy3dData`] Mapping of monitor name to :class:`Tidy3dData` intance. - log_string : ``str``, optional - string containing the log from server. + log_string : str = None + A string containing the log information from the simulation run. """ simulation: Simulation @@ -494,21 +481,21 @@ class SimulationData(Tidy3dBaseModel): @property def log(self): - """prints the server-side log.""" + """Prints the server-side log.""" print(self.log_string if self.log_string else "no log stored") def __getitem__(self, monitor_name: str) -> Union[xr.DataArray, xr.Dataset]: - """get the :class:`MonitorData` xarray representation by name (``sim_data[monitor_name]``). + """Get the :class:`MonitorData` xarray representation by name (``sim_data[monitor_name]``). Parameters ---------- monitor_name : ``str`` - Name of :class:`Monitor` to return data for. + Name of the :class:`Monitor` to return data for. Returns ------- - ``Union[xarray.DataArray``, xarray.Dataset]`` - The ``xarray`` representation of the data. + xarray.DataArray or xarray.Dataset + The xarray representation of the data. """ monitor_data = self.monitor_data.get(monitor_name) if not monitor_data: @@ -590,8 +577,8 @@ def export(self, fname: str) -> None: Parameters ---------- - fname : ``str`` - Path to data file (including filename). + fname : str + Path to .hdf5 data file (including filename). """ with h5py.File(fname, "a") as f_handle: @@ -616,8 +603,8 @@ def load(cls, fname: str): Parameters ---------- - fname : ``str`` - Path to data file (including filename). + fname : str + Path to .hdf5 data file (including filename). Returns ------- @@ -651,7 +638,7 @@ def load(cls, fname: str): ) def __eq__(self, other): - """check equality against another SimulationData instance + """Check equality against another :class:`SimulationData` instance. Parameters ---------- diff --git a/tidy3d/components/geometry.py b/tidy3d/components/geometry.py index 25aaa7009f..55d5ae01c1 100644 --- a/tidy3d/components/geometry.py +++ b/tidy3d/components/geometry.py @@ -1,4 +1,4 @@ -"""defines objects in space""" +"""Defines spatial extent of objects.""" from abc import ABC, abstractmethod from typing import List, Tuple, Union, Any @@ -14,7 +14,11 @@ from .types import Vertices, Ax, Shapely from .viz import add_ax_if_none -PLOT_BUFFER = 0.3 # add this around extents of .visualize() +# add this around extents of plots +PLOT_BUFFER = 0.3 + + +# TODO: GDS file importing. class Geometry(Tidy3dBaseModel, ABC): @@ -22,24 +26,22 @@ class Geometry(Tidy3dBaseModel, ABC): center: Coordinate = (0.0, 0.0, 0.0) - """ volume and intersections """ - def inside(self, x, y, z) -> bool: - """Returns true if point ``(x,y,z)`` inside volume of geometry. + """Returns ``True`` if point ``(x,y,z)`` is inside volume of :class:`Geometry`. Parameters ---------- - x : ``float`` + x : float Position of point in x direction. - y : ``float`` + y : float Position of point in y direction. - z : ``float`` + z : float Position of point in z direction. Returns ------- - ``bool`` - Whether point ``(x,y,z)`` is inside geometry. + bool + True if point ``(x,y,z)`` is inside geometry. """ shapes_intersect = self.intersections(z=z) loc = Point(x, y) @@ -47,25 +49,26 @@ def inside(self, x, y, z) -> bool: @abstractmethod def intersections(self, x: float = None, y: float = None, z: float = None) -> List[Shapely]: - """Returns list of shapely geoemtries at plane specified by one non-None value of x,y,z + """Returns list of shapely geoemtries at plane specified by one non-None value of x,y,z. Parameters ---------- - x : ``float``, optional - Position of plane in x direction. - y : ``float``, optional - Position of plane in y direction. - z : ``float``, optional - Position of plane in z direction. + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. Returns ------- - ``[shapely.geometry.base.BaseGeometry]`` + List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ def intersects(self, other) -> bool: - """Determines whether two :class:`Geometry` have intersecting `.bounds`. + """Returns ``True`` if two :class:`Geometry` have intersecting `.bounds`. Parameters ---------- @@ -74,7 +77,7 @@ def intersects(self, other) -> bool: Returns ------- - ``bool`` + bool Whether the rectangular bounding boxes of the two geometries intersect. """ @@ -95,30 +98,28 @@ def intersects_plane(self, x: float = None, y: float = None, z: float = None) -> Parameters ---------- - x : ``float``, optional - Position of plane in x direction. - y : ``float``, optional - Position of plane in y direction. - z : ``float``, optional - Position of plane in z direction. + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. Returns ------- - ``bool`` - Whether this geometry intersects the plane + bool + Whether this geometry intersects the plane. """ intersections = self.intersections(x=x, y=y, z=z) return bool(intersections) - """ Bounding boxes """ - @property def bounds(self) -> Bound: # pylint:disable=too-many-locals - """Returns bounding box for geometry. + """Returns bounding box min and max coordinates.. Returns ------- - ``(float, float, float), (float, float float)`` + Tuple[float, float, float], Tuple[float, float float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. """ @@ -139,7 +140,7 @@ def bounds(self) -> Bound: # pylint:disable=too-many-locals @property def bounding_box(self): - """Returns :class:`Box` representation of ``self.bounds``. + """Returns :class:`Box` representation of the bounding box of a :class:`Geometry`. Returns ------- @@ -156,26 +157,24 @@ def bounding_box(self): return Box(center=(x0, y0, z0), size=(Lx, Ly, Lz)) def _pop_bounds(self, axis: Axis) -> Tuple[Coordinate2D, Tuple[Coordinate2D, Coordinate2D]]: - """Returns min and max bounds in plane normal to and tangential to `axis` + """Returns min and max bounds in plane normal to and tangential to ``axis``. Parameters ---------- - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). Returns ------- - ``(float, float), ((float, float), (float, float))`` + Tuple[float, float], Tuple[Tuple[float, float], Tuple[float, float]] Bounds along axis and a tuple of bounds in the ordered planar coordinates. - Packed as ``(zmin, zmax), ((xmin, ymin), (xmax, ymax))`` + Packed as ``(zmin, zmax), ((xmin, ymin), (xmax, ymax))``. """ b_min, b_max = self.bounds zmin, (xmin, ymin) = self.pop_axis(b_min, axis=axis) zmax, (xmax, ymax) = self.pop_axis(b_max, axis=axis) return (zmin, zmax), ((xmin, ymin), (xmax, ymax)) - """ Plotting """ - @add_ax_if_none def plot( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **patch_kwargs @@ -184,43 +183,51 @@ def plot( Parameters ---------- - x : ``float``, optional - Position of plane in x direction. - y : ``float``, optional - Position of plane in y direction. - z : ``float``, optional - Position of plane in z direction. - ax : ``matplotlib.axes._subplots.Axes``, optional - matplotlib axes to plot on, if not specified, one is created. + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. **patch_kwargs - Optional keyword arguments passed to ``add_artist(patch, **patch_kwargs)``. + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. #pylint:disable=line-too-long Returns ------- - ``matplotlib.axes._subplots.Axes`` + matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + + # find shapes that intersect self at plane axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) shapes_intersect = self.intersections(x=x, y=y, z=z) + + # for each intersection, plot the shape for shape in shapes_intersect: patch = PolygonPatch(shape, **patch_kwargs) ax.add_artist(patch) + + # clean up the axis display ax = self.add_ax_labels_lims(axis=axis, ax=ax) ax.set_aspect("equal") ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") return ax def _get_plot_labels(self, axis: Axis) -> Tuple[str, str]: - """returns correct x and y axis labels for cross section plots + """Returns planar coordinate x and y axis labels for cross section plots. Parameters ---------- - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). Returns ------- - ``(str, str)`` + str, str Labels of plot, packaged as ``(xlabel, ylabel)``. """ _, (xlabel, ylabel) = self.pop_axis("xyz", axis=axis) @@ -229,18 +236,18 @@ def _get_plot_labels(self, axis: Axis) -> Tuple[str, str]: def _get_plot_limits( self, axis: Axis, buffer: float = PLOT_BUFFER ) -> Tuple[Coordinate2D, Coordinate2D]: - """gets (xmin, ymin, xmax, ymax) limits for cross section plots + """Gets planar coordinate limits for cross section plots. Parameters ---------- - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). - buffer : ``float``, optional - Amount of space to place around the limits + buffer : float = 0.3 + Amount of space to add around the limits on the + and - sides. Returns ------- - ``(float, float), (float, float)`` + Tuple[float, float], Tuple[float, float] The x and y plot limits, packed as ``(xmin, xmax), (ymin, ymax)``. """ _, ((xmin, ymin), (xmax, ymax)) = self._pop_bounds(axis=axis) @@ -251,16 +258,16 @@ def add_ax_labels_lims(self, axis: Axis, ax: Ax, buffer: float = PLOT_BUFFER) -> Parameters ---------- - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). - ax : ``matplotlib.axes._subplots.Axes`` - matplotlib axes to add labels and limits on. - buffer : ``float``, optional - Amount of space to place around the limits + ax : matplotlib.axes._subplots.Axes + Matplotlib axes to add labels and limits on. + buffer : float = 0.3 + Amount of space to place around the limits on the + and - sides. Returns ------- - ``matplotlib.axes._subplots.Axes`` + matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ xlabel, ylabel = self._get_plot_labels(axis=axis) @@ -271,24 +278,23 @@ def add_ax_labels_lims(self, axis: Axis, ax: Ax, buffer: float = PLOT_BUFFER) -> ax.set_ylabel(ylabel) return ax - """ Utility """ - @staticmethod def pop_axis(coord: Tuple[Any, Any, Any], axis: int) -> Tuple[Any, Tuple[Any, Any]]: """Separates coordinate at ``axis`` index from coordinates on the plane tangent to ``axis``. Parameters ---------- - coord : ``(Any, Any, Any)`` + coord : Tuple[Any, Any, Any] Tuple of three values in original coordinate system. - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). Returns ------- - ``Any, (Any, Any)`` - The coordinates separated into that in the axis and those in the planar dimensions. - Packaged as ``axis_coord, (planar_coord1, planar_coord2)``. + Any, Tuple[Any, Any] + The input coordinates are separated into the one along the axis provided + and the two on the planar coordinates, + like ``axis_coord, (planar_coord1, planar_coord2)``. """ plane_vals = list(coord) axis_val = plane_vals.pop(axis) @@ -296,21 +302,21 @@ def pop_axis(coord: Tuple[Any, Any, Any], axis: int) -> Tuple[Any, Tuple[Any, An @staticmethod def unpop_axis(ax_coord: Any, plane_coords: Tuple[Any, Any], axis: int) -> Tuple[Any, Any, Any]: - """Combine coordinate from `axis` index with coordinates on the plane tangent to `axis`. + """Combine coordinate along axis with coordinates on the plane tangent to the axis. Parameters ---------- - ax_coord : ``Any`` - Value along ``axis`` direction. - plane_coords : ``(Any, Any)`` + ax_coord : Any + Value along axis direction. + plane_coords : Tuple[Any, Any] Values along ordered planar directions. - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). Returns ------- - ``(Any, Any, Any)`` - The three values in original coordinate system. + Tuple[Any, Any, Any] + The three values in the xyz coordinate system. """ coords = list(plane_coords) coords.insert(axis, ax_coord) @@ -318,21 +324,21 @@ def unpop_axis(ax_coord: Any, plane_coords: Tuple[Any, Any], axis: int) -> Tuple @staticmethod def _parse_xyz_kwargs(**xyz) -> Tuple[Axis, float]: - """Turns x,y,z kwargs into plane axis and position. + """Turns x,y,z kwargs into index of the normal axis and position along that axis. Parameters ---------- - x : ``float`` - Position of point in x direction. - y : ``float`` - Position of point in y direction. - z : ``float`` - Position of point in z direction. + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. Returns ------- - ``(int, float)`` - Index into xyz axis (0,1,2) and position along axis. + int, float + Index into xyz axis (0,1,2) and position along that axis. """ xyz_filtered = {k: v for k, v in xyz.items() if v is not None} assert len(xyz_filtered) == 1, "exatly one kwarg in [x,y,z] must be specified." @@ -341,7 +347,7 @@ def _parse_xyz_kwargs(**xyz) -> Tuple[Axis, float]: return axis, position -""" abstract subclasses """ +""" Abstract subclasses """ class Planar(Geometry, ABC): @@ -351,21 +357,22 @@ class Planar(Geometry, ABC): length: pydantic.NonNegativeFloat = None def intersections(self, x: float = None, y: float = None, z: float = None): - """returns shapely geometry at plane specified by one non None value of x,y,z + """Returns shapely geometry at plane specified by one non None value of x,y,z. Parameters ---------- - x : ``float`` - Position of point in x direction. - y : ``float`` - Position of point in y direction. - z : ``float`` - Position of point in z direction. + x : float + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float + Position of plane in z direction, only one of x,y,z can be specified to define plane. Returns ------- - ``[shapely.geometry.base.BaseGeometry]`` + List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) if axis == self.axis: @@ -377,31 +384,31 @@ def intersections(self, x: float = None, y: float = None, z: float = None): @abstractmethod def _intersections_normal(self) -> list: - """Find shapely geometries intersecting planar geometry with axis normal to slab + """Find shapely geometries intersecting planar geometry with axis normal to slab. Returns ------- - ``list[shapely.geometry.base.BaseGeometries]`` - List containing the shapely representation of the normal cross section of the planar - geometry. + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ @abstractmethod def _intersections_side(self, position: float, axis: Axis) -> list: - """Find shapely geometries intersecting planar geometry with axis orthogonal to plane + """Find shapely geometries intersecting planar geometry with axis orthogonal to plane. Parameters ---------- - position : ``float`` - Position along ``axis`` - axis : ``int`` + position : float + Position along axis. + axis : int Integer index into 'xyz' (0,1,2). Returns ------- - ``list[shapely.geometry.base.BaseGeometries]`` - List of 2D geometries intersecting with planar geometry at ``position`` along side - ``axis``. + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ @property @@ -410,7 +417,7 @@ def bounds(self): Returns ------- - ``(float, float, float), (float, float float)`` + Tuple[float, float, float], Tuple[float, float float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. """ z0, _ = self.pop_axis(self.center, axis=self.axis) @@ -430,11 +437,11 @@ def _order_by_axis(self, plane_val: Any, axis_val: Any, axis: int) -> Tuple[Any, Parameters ---------- - plane_val : ``Any`` + plane_val : Any The value in the planar coordinate. - axis_val : ``Any`` + axis_val : Any The value in the ``axis`` coordinate. - axis : ``int`` + axis : int Integer index into the structure's planar axis. Returns @@ -454,19 +461,19 @@ class Circular(Geometry): radius: pydantic.NonNegativeFloat def _intersect_dist(self, position, z0) -> float: - """distance between points on circle at z=position where center of circle at z=z0 + """Distance between points on circle at z=position where center of circle at z=z0. Parameters ---------- - position : ``float`` + position : float position along z. - z0 : ``float`` + z0 : float center of circle in z. Returns ------- - ``float`` - distance between points on the circle intersecting z=z, if no points, ``None``. + float + Distance between points on the circle intersecting z=z, if no points, ``None``. """ dz = np.abs(z0 - position) if dz > self.radius: @@ -483,9 +490,9 @@ class Box(Geometry): Parameters ---------- - center : ``(float, float, float)`` - center of box in x,y,z. Defaults to ``(0,0,0)``. - size : ``(float, float, float)`` + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of box in x,y,z. + size : Tuple[float, float, float] Size of box in x,y,z. Example @@ -496,21 +503,22 @@ class Box(Geometry): size: Size def intersections(self, x: float = None, y: float = None, z: float = None): - """returns shapely geoemtry at plane specified by one non None value of x,y,z + """Returns shapely geometry at plane specified by one non None value of x,y,z. Parameters ---------- - x : ``float`` - Position of point in x direction. - y : ``float`` - Position of point in y direction. - z : ``float`` - Position of point in z direction. + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. Returns ------- - ``[shapely.geometry.base.BaseGeometry]`` + List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) z0, (x0, y0) = self.pop_axis(self.center, axis=axis) @@ -521,20 +529,20 @@ def intersections(self, x: float = None, y: float = None, z: float = None): return [box(minx=x0 - Lx / 2, miny=y0 - Ly / 2, maxx=x0 + Lx / 2, maxy=y0 + Ly / 2)] def inside(self, x, y, z) -> bool: - """Returns true if point ``(x,y,z)`` inside volume of geometry. + """Returns ``True`` if point ``(x,y,z)`` inside volume of geometry. Parameters ---------- - x : ``float`` + x : float Position of point in x direction. - y : ``float`` + y : float Position of point in y direction. - z : ``float`` + z : float Position of point in z direction. Returns ------- - ``bool`` + bool Whether point ``(x,y,z)`` is inside geometry. """ x0, y0, z0 = self.center @@ -546,11 +554,11 @@ def inside(self, x, y, z) -> bool: @property def bounds(self) -> Bound: - """Returns bounding box for geometry + """Returns bounding box min and max coordinates. Returns ------- - ``(float, float, float), (float, float float)`` + Tuple[float, float, float], Tuple[float, float float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. """ size = self.size @@ -561,7 +569,7 @@ def bounds(self) -> Bound: @property def geometry(self): - """:class:`Box` representation of self (used for subclasses of Box) + """:class:`Box` representation of self (used for subclasses of Box). Returns ------- @@ -572,13 +580,13 @@ def geometry(self): class Sphere(Circular): - """Sphere geometry. + """Spherical geometry. Parameters ---------- - center : ``(float, float, float)`` - center of sphere in x,y,z. Defaults to ``(0,0,0)``. - radius : ``float`` + center : Tuple[float, float, float] = 0.0, 0.0, 0.0 + Center of sphere in x,y,z. + radius : float Radius of sphere. Example @@ -589,15 +597,15 @@ class Sphere(Circular): type: Literal["Sphere"] = "Sphere" def inside(self, x, y, z) -> bool: - """Returns true if point ``(x,y,z)`` inside volume of geometry. + """Returns True if point ``(x,y,z)`` inside volume of geometry. Parameters ---------- - x : ``float`` + x : float Position of point in x direction. - y : ``float`` + y : float Position of point in y direction. - z : ``float`` + z : float Position of point in z direction. Returns @@ -612,21 +620,22 @@ def inside(self, x, y, z) -> bool: return (dist_x ** 2 + dist_y ** 2 + dist_z ** 2) <= (self.radius ** 2) def intersections(self, x: float = None, y: float = None, z: float = None): - """returns shapely geoemtry at plane specified by one non None value of x,y,z + """Returns shapely geometry at plane specified by one non None value of x,y,z. Parameters ---------- - x : ``float``, optional - Description - y : ``float``, optional - Description - z : ``float``, optional - Description + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. Returns ------- - ``[shapely.geometry.base.BaseGeometry]`` + List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) z0, (x0, y0) = self.pop_axis(self.center, axis=axis) @@ -637,11 +646,11 @@ def intersections(self, x: float = None, y: float = None, z: float = None): @property def bounds(self): - """Returns bounding box for geometry + """Returns bounding box min and max coordinates. Returns ------- - ``(float, float, float), (float, float float)`` + Tuple[float, float, float], Tuple[float, float, float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. """ coord_min = tuple(c - self.radius for c in self.center) @@ -650,18 +659,18 @@ def bounds(self): class Cylinder(Circular, Planar): - """Cylinder geometry. + """Cylindrical geometry. Parameters ---------- - center : ``(float, float, float)`` - center of cylinder in x,y,z. Defaults to ``(0,0,0)``. - radius : ``float`` + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) + center of cylinder in x,y,z. + radius : float Radius of cylinder. - length : ``float`` - Length of sphere along axis. - axis : ``int`` - Integer index into the cylinder's ``length`` axis (0,1,2) -> (x,y,z) + length : float + Length of cylinder along axis. + axis : int + Cylinder's length axis index (0, 1, 2) -> (x, y, z) Example ------- @@ -672,31 +681,32 @@ class Cylinder(Circular, Planar): type: Literal["Cylinder"] = "Cylinder" def _intersections_normal(self): - """Find shapely geometries intersecting cylindrical geometry with axis normal to slab + """Find shapely geometries intersecting cylindrical geometry with axis normal to slab. Returns ------- - ``list[shapely.geometry.base.BaseGeometries]`` - List containing the shapely representation of the polygon. + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ _, (x0, y0) = self.pop_axis(self.center, axis=self.axis) return [Point(x0, y0).buffer(self.radius)] def _intersections_side(self, position, axis): - """Find shapely geometries intersecting cylindrical geometry with axis orthogonal to length + """Find shapely geometries intersecting cylindrical geometry with axis orthogonal to length. Parameters ---------- - position : ``float`` - Position along ``axis`` - axis : ``int`` - Integer index into 'xyz' (0,1,2). + position : float + Position along axis direction. + axis : int + Integer index into 'xyz' (0, 1, 2). Returns ------- - ``list[shapely.geometry.base.BaseGeometries]`` - List of 2D geometries intersecting with cylinder geometry at ``position`` along side - ``axis``. + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ z0_axis, _ = self.pop_axis(self.center, axis=self.axis) intersect_dist = self._intersect_dist(position, z0_axis) @@ -704,30 +714,29 @@ def _intersections_side(self, position, axis): return [] Lx, Ly = self._order_by_axis(plane_val=intersect_dist, axis_val=self.length, axis=axis) _, (x0_plot_plane, y0_plot_plane) = self.pop_axis(self.center, axis=axis) - return [ - box( - minx=x0_plot_plane - Lx / 2, - miny=y0_plot_plane - Ly / 2, - maxx=x0_plot_plane + Lx / 2, - maxy=y0_plot_plane + Ly / 2, - ) - ] + int_box = box( + minx=x0_plot_plane - Lx / 2, + miny=y0_plot_plane - Ly / 2, + maxx=x0_plot_plane + Lx / 2, + maxy=y0_plot_plane + Ly / 2, + ) + return [int_box] def inside(self, x, y, z) -> bool: - """Returns true if point ``(x,y,z)`` inside volume of geometry. + """Returns True if point ``(x,y,z)`` inside volume of geometry. Parameters ---------- - x : ``float`` + x : float Position of point in x direction. - y : ``float`` + y : float Position of point in y direction. - z : ``float`` + z : float Position of point in z direction. Returns ------- - ``bool`` + bool Whether point ``(x,y,z)`` is inside geometry. """ z0, (x0, y0) = self.pop_axis(self.center, axis=self.axis) @@ -740,11 +749,11 @@ def inside(self, x, y, z) -> bool: @property def _bounds(self): - """Returns bounding box for geometry + """Returns bounding box min and max coordinates. Returns ------- - ``(float, float, float), (float, float float)`` + Tuple[float, float, float], Tuple[float, float, float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. """ coord_min = list(c - self.radius for c in self.center) @@ -755,16 +764,16 @@ def _bounds(self): class PolySlab(Planar): - """Polygon with constant thickness along 3rd axis. + """Polygon with constant thickness (slab) along axis direction. Parameters ---------- - vertices : ``[(float, float)]`` - List of (x,y) vertices defining the polygon face. - axis : ``int`` + vertices : List[Tuple[float, float]] + List of vertices defining the polygon face along dimensions parallel to slab normal axis. + axis : int Integer index into the polygon's slab axis. (0,1,2) -> (x,y,z) - slab_bounds: ``(float, float)`` - Minimum and maximum position in slab axis. + slab_bounds: Tuple[float, float] + Minimum and maximum positions of the slab along axis. Example ------- @@ -793,20 +802,20 @@ def set_center(cls, val, values): return val def inside(self, x, y, z) -> bool: # pylint:disable=too-many-locals - """Returns true if point ``(x,y,z)`` inside volume of geometry. + """Returns True if point ``(x,y,z)`` inside volume of geometry. Parameters ---------- - x : ``float`` + x : float Position of point in x direction. - y : ``float`` + y : float Position of point in y direction. - z : ``float`` + z : float Position of point in z direction. Returns ------- - ``bool`` + bool Whether point ``(x,y,z)`` is inside geometry. """ z0, _ = self.pop_axis(self.center, axis=self.axis) @@ -836,8 +845,9 @@ def _intersections_normal(self): Returns ------- - ``list[shapely.geometry.base.BaseGeometries]`` - List containing the shapely representation of the polygon. + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ return [Polygon(self.vertices)] @@ -846,16 +856,16 @@ def _intersections_side(self, position, axis) -> list: # pylint:disable=too-man Parameters ---------- - position : ``float`` + position : float Position along ``axis`` - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). Returns ------- - ``list[shapely.geometry.base.BaseGeometries]`` - List of 2D geometries intersecting with planar geometry at ``position`` along side - ``axis``. + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to `Shapely's Documentaton `_. """ z0, _ = self.pop_axis(self.center, axis=self.axis) @@ -878,17 +888,17 @@ def _find_intersecting_vertices( self, position: float, axis: int ) -> Tuple[np.ndarray, np.ndarray]: """Finds pairs of forward and backwards vertices where polygon intersects position at axis. - Assumed xy plane. + Assumes axis is handles so this function works on xy plane. Parameters ---------- - position : ``float`` + position : float position along axis - axis : ``int`` + axis : int Integer index into 'xyz' (0,1,2). Returns - ``(np.ndarray, np.ndarray)`` + np.ndarray, np.ndarray Backward (xy) vertices and forward (xy) vertices. """ @@ -914,16 +924,21 @@ def _find_intersecting_vertices( def _find_intersecting_ys( iverts_b: np.ndarray, iverts_f: np.ndarray, position: float ) -> List[float]: - """For each intersecting segment, find intersection point (in y) assuming straight line + """For each intersecting segment, find intersection point (in y) assuming straight line. Parameters ---------- - iverts_b : ``np.ndarray`` - backward (x,y) vertices - iverts_f : ``np.ndarray`` - forward (x,y) vertices - position : ``float`` - position along coordinate x + iverts_b : np.ndarray + Backward (x,y) vertices. + iverts_f : np.ndarray + Forward (x,y) vertices. + position : float + Position along coordinate x. + + Returns + ------- + List[float] + List of intersection points along y direction. """ ints_y = [] @@ -938,11 +953,11 @@ def _find_intersecting_ys( @property def _bounds(self): - """Returns bounding box for geometry + """Returns bounding box min and max coordinates. Returns ------- - ``(float, float, float), (float, float float)`` + Tuple[float, float, float], Tuple[float, float, float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. """ @@ -959,5 +974,6 @@ def _bounds(self): return (tuple(coords_min), tuple(coords_max)) +# geometries that can be used to define structures. GeometryFields = (Box, Sphere, Cylinder, PolySlab) GeometryType = Union[GeometryFields] diff --git a/tidy3d/components/grid.py b/tidy3d/components/grid.py index 1eca81821c..28f6ce03cf 100644 --- a/tidy3d/components/grid.py +++ b/tidy3d/components/grid.py @@ -1,25 +1,24 @@ -""" defines the FDTD grid """ +"""Defines the FDTD grid.""" import numpy as np from .base import Tidy3dBaseModel from .types import Array, Axis -""" Grid data """ -# type of one dimensional coordinate array +# data type of one dimensional coordinate array. Coords1D = Array[float] class Coords(Tidy3dBaseModel): - """Holds data about a set of x,y,z positions on a grid + """Holds data about a set of x,y,z positions on a grid. Parameters ---------- - x : ``np.ndarray`` + x : np.ndarray Positions of coordinates along x direction. - y : ``np.ndarray`` + y : np.ndarray Positions of coordinates along y direction. - z : ``np.ndarray`` + z : np.ndarray Positions of coordinates along z direction. Example @@ -79,6 +78,7 @@ class YeeGrid(Tidy3dBaseModel): >>> coords = Coords(x=x, y=y, z=z) >>> field_grid = FieldGrid(x=coords, y=coords, z=coords) >>> yee_grid = YeeGrid(E=field_grid, H=field_grid) + >>> Ex_coords = yee_grid.E.x """ E: FieldGrid @@ -86,7 +86,7 @@ class YeeGrid(Tidy3dBaseModel): class Grid(Tidy3dBaseModel): - """contains all information about the spatial positions of the FDTD grid + """Contains all information about the spatial positions of the FDTD grid. Parameters ---------- @@ -100,51 +100,104 @@ class Grid(Tidy3dBaseModel): >>> z = np.linspace(-1, 1, 12) >>> coords = Coords(x=x, y=y, z=z) >>> grid = Grid(boundaries=coords) + >>> centers = grid.centers + >>> sizes = grid.sizes + >>> yee_grid = grid.yee """ boundaries: Coords @staticmethod def _avg(coords1d: Coords1D): - """average an array of 1D coordinates""" + """Return average positions of an array of 1D coordinates.""" return (coords1d[1:] + coords1d[:-1]) / 2.0 @staticmethod def _min(coords1d: Coords1D): - """get minus positions of 1D coordinates""" + """Return minus positions of 1D coordinates.""" return coords1d[:-1] @property def centers(self) -> Coords: - """get centers of the cells in the :class:`Grid`. + """Return centers of the cells in the :class:`Grid`. Returns ------- :class:`Coords` centers of the FDTD cells in x,y,z stored as :class:`Coords` object. + + Example + ------- + >>> x = np.linspace(-1, 1, 10) + >>> y = np.linspace(-1, 1, 11) + >>> z = np.linspace(-1, 1, 12) + >>> coords = Coords(x=x, y=y, z=z) + >>> grid = Grid(boundaries=coords) + >>> centers = grid.centers """ return Coords(**{key: self._avg(val) for key, val in self.boundaries.dict().items()}) @property - def cell_sizes(self) -> Coords: - """get sizes of the cells in the :class:`Grid`. + def sizes(self) -> Coords: + """Return sizes of the cells in the :class:`Grid`. Returns ------- :class:`Coords` Sizes of the FDTD cells in x,y,z stored as :class:`Coords` object. + + Example + ------- + >>> x = np.linspace(-1, 1, 10) + >>> y = np.linspace(-1, 1, 11) + >>> z = np.linspace(-1, 1, 12) + >>> coords = Coords(x=x, y=y, z=z) + >>> grid = Grid(boundaries=coords) + >>> sizes = grid.sizes """ return Coords(**{key: np.diff(val) for key, val in self.boundaries.dict().items()}) + @property + def _primal_steps(self) -> Coords: + """Return primal steps of the cells in the :class:`Grid`. + + Returns + ------- + :class:`Coords` + Distances between each of the cell boundaries along each dimension. + """ + return self.sizes + + @property + def _dual_steps(self) -> Coords: + """Return dual steps of the cells in the :class:`Grid`. + + Returns + ------- + :class:`Coords` + Distances between each of the cell centers along each dimension. + """ + return Coords(**{key: np.diff(val) for key, val in self.centers.dict().items()}) + @property def yee(self) -> YeeGrid: - """return the :class:`YeeGrid` defining the yee cell locations for this :class:`Grid`. + """Return the :class:`YeeGrid` defining the yee cell locations for this :class:`Grid`. Returns ------- :class:`YeeGrid` - Coordinates of all of the components on the yee lattice. + Stores coordinates of all of the components on the yee lattice. + + Example + ------- + >>> x = np.linspace(-1, 1, 10) + >>> y = np.linspace(-1, 1, 11) + >>> z = np.linspace(-1, 1, 12) + >>> coords = Coords(x=x, y=y, z=z) + >>> grid = Grid(boundaries=coords) + >>> yee_cells = grid.yee + >>> Ex_positions = yee_cells.E.x """ yee_e_kwargs = {key: self._yee_e(axis=axis) for axis, key in enumerate("xyz")} yee_h_kwargs = {key: self._yee_h(axis=axis) for axis, key in enumerate("xyz")} @@ -153,8 +206,8 @@ def yee(self) -> YeeGrid: yee_h = FieldGrid(**yee_h_kwargs) return YeeGrid(E=yee_e, H=yee_h) - def _yee_e(self, axis: Axis): # - """E field yee lattice sites for axis""" + def _yee_e(self, axis: Axis): + """E field yee lattice sites for axis.""" boundary_coords = self.boundaries.dict() @@ -168,7 +221,7 @@ def _yee_e(self, axis: Axis): # return Coords(**yee_coords) def _yee_h(self, axis: Axis): - """E field yee lattice sites for axis""" + """E field yee lattice sites for axis.""" boundary_coords = self.boundaries.dict() diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index af57fbfd33..3b5f27a2a1 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -11,14 +11,24 @@ from .viz import add_ax_if_none from .validators import validate_name_str -from ..constants import C_0, inf +from ..constants import C_0, inf, pec_val from ..log import log + """ Medium Definitions """ class AbstractMedium(ABC, Tidy3dBaseModel): - """A medium within which electromagnetic waves propagate""" + """A medium within which electromagnetic waves propagate. + + Parameters + ---------- + frequeuncy_range : Tuple[float, float] = (-inf, inf) + Range of validity for the medium in Hz. + If simulation or plotting functions use frequency out of this range, a warning is thrown. + name : str = None + Optional name for the medium. + """ name: str = None frequency_range: Tuple[FreqBound, FreqBound] = (-inf, inf) @@ -27,18 +37,36 @@ class AbstractMedium(ABC, Tidy3dBaseModel): @abstractmethod def eps_model(self, frequency: float) -> complex: - """complex permittivity as a function of frequency + """Complex-valued permittivity as a function of frequency. Parameters ---------- frequency : float - Description - name: ``str`` + Frequency to evaluate permittivity at (Hz). + + Returns + ------- + complex + Complex-valued relative permittivity evaluated at ``frequency``. """ @add_ax_if_none def plot(self, freqs: float, ax: Ax = None) -> Ax: # pylint: disable=invalid-name - """plot n, k of medium as a function of frequencies.""" + """Plot n, k of a :class:`Medium` as a function of frequency. + + Parameters + ---------- + freqs: float + Frequencies (Hz) to evaluate the medium properties at. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + freqs = np.array(freqs) eps_complex = self.eps_model(freqs) n, k = eps_complex_to_nk(eps_complex) @@ -71,9 +99,62 @@ def _eps_model(self, frequency: float) -> complex: """ Dispersionless Medium """ +# PEC keyword +class PECMedium(AbstractMedium): + """Perfect electrical conductor class. + + Note + ---- + To avoid confusion from duplicate PECs, + use the pre-defined instance ``PEC`` rather than creating your own :class:`PECMedium` instance. + """ + + def eps_model(self, frequency: float) -> complex: + """Complex-valued permittivity as a function of frequency. + + Parameters + ---------- + frequency : float + Frequency to evaluate permittivity at (Hz). + + Returns + ------- + complex + Complex-valued relative permittivity evaluated at ``frequency``. + """ + + # return something like frequency with value of pec_val + 0j + return 0j * frequency + pec_val + + +# PEC instance (usable) +PEC = PECMedium(name="PEC") + class Medium(AbstractMedium): - """Dispersionless medium.""" + """Dispersionless medium. + + Parameters + ---------- + permittivity : float = 1.0 + Relative permittivity in dimensionless units. + Must be greater than or equal to 1. + conductivity : float = 0.0 + Electric conductivity in dimensions of (S/micron) + Defined such that the imaginary part of the complex permittivity at angular frequency omega + is given by conductivity/omega. + Must be greater than or equal to 0. + frequeuncy_range : Tuple[float, float] = (-inf, inf) + Range of validity for the medium in Hz. + If simulation or plotting functions use frequency out of this range, a warning is thrown. + name : str = None + Optional name for the medium. + + Example + ------- + >>> dielectric = Medium(permittivity=4.0, name='my_medium') + >>> eps = dielectric.eps_model(200e12) + """ permittivity: pydantic.confloat(ge=1.0) = 1.0 conductivity: pydantic.confloat(ge=0.0) = 0.0 @@ -85,12 +166,12 @@ def eps_model(self, frequency: float) -> complex: Parameters ---------- - frequency : ``float`` + frequency : float Frequency to evaluate permittivity at (Hz). Returns ------- - ``complex`` + complex Complex-valued relative permittivity evaluated at ``frequency``. """ return eps_sigma_to_eps_complex(self.permittivity, self.conductivity, frequency) @@ -105,27 +186,129 @@ def __str__(self) -> str: ) +class AnisotropicMedium(AbstractMedium): + """Diagonally anisotripic medium. + + Parameters + ---------- + xx : :class:`Medium` + :class:`Medium` describing the :math:`\\epsilon_{xx}`-component of the permittivity tensor. + yy : :class:`Medium` + :class:`Medium` describing the :math:`\\epsilon_{yy}`-component of the permittivity tensor. + zz : :class:`Medium` + :class:`Medium` describing the :math:`\\epsilon_{zz}`-component of the permittivity tensor. + name : str = None + Optional name for the medium. + + Note + ---- + Only diagonal anisotropy and non-dispersive components are currently supported. + + Example + ------- + >>> medium_xx = Medium(permittivity=4.0) + >>> medium_yy = Medium(permittivity=4.1) + >>> medium_zz = Medium(permittivity=3.9) + >>> anisotropic_dielectric = AnisotropicMedium(xx=medium_xx, yy=medium_yy, zz=medium_zz) + """ + + xx: Medium + yy: Medium + zz: Medium + + @ensure_freq_in_range + def eps_model(self, frequency: float) -> complex: + """Complex-valued permittivity as a function of frequency. + + Parameters + ---------- + frequency : float + Frequency to evaluate permittivity at (Hz). + + Returns + ------- + Tuple[complex, complex, complex] + Complex-valued relative permittivity for each component evaluated at ``frequency``. + """ + eps_xx = self.xx.eps_model(frequency) + eps_yy = self.yy.eps_model(frequency) + eps_zz = self.zz.eps_model(frequency) + return (eps_xx, eps_yy, eps_zz) + + @add_ax_if_none + def plot(self, freqs: float, ax: Ax = None) -> Ax: + """Plot n, k of a :class:`Medium` as a function of frequency. + + Parameters + ---------- + freqs: float + Frequencies (Hz) to evaluate the medium properties at. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + freqs = np.array(freqs) + freqs_thz = freqs / 1e12 + + for label, medium_component in zip(("xx", "yy", "zz"), (self.xx, self.yy, self.zz)): + + eps_complex = medium_component.eps_model(freqs) + n, k = eps_complex_to_nk(eps_complex) + ax.plot(freqs_thz, n, label=f"n, eps_{label}") + ax.plot(freqs_thz, k, label=f"k, eps_{label}") + + ax.set_xlabel("frequency (THz)") + ax.set_title("medium dispersion") + ax.legend() + ax.set_aspect("auto") + return ax + + """ Dispersive Media """ class DispersiveMedium(AbstractMedium, ABC): """A Medium with dispersion (propagation characteristics depend on frequency)""" + @property + @abstractmethod + def pole_residue(self): + """Representation of Medium as a pole-residue model.""" + class PoleResidue(DispersiveMedium): - """defines a dispersion model through pole-residue pair model. + """A dispersive medium described by the pole-residue pair model. + The frequency-dependence of the complex-valued permittivity is described by: + + .. math:: + + \\epsilon(\\omega) = \\epsilon_\\infty - \\sum_i + \\left[\\frac{c_i}{j \\omega + a_i} + + \\frac{c_i^*}{j \\omega + a_i^*}\\right] + + where :math:`a_i` is in Hz and :math:`c_i` is unitless. Parameters ---------- - eps_inf : ``float`` = 1.0 - Permittivity at infinite frequency. - poles : ``[((float, float), (float, float))]`` = [] - List of poles, ``poles[0]`` contain the real and imaginary parts of - the frequency and amplitude of the first, respecitvely. + eps_inf : float = 1.0 + Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`). + poles : List[Tuple[complex, complex]] + List of complex-valued (:math:`a_i, c_i`) poles for the model. + frequeuncy_range : Tuple[float, float] = (-inf, inf) + Range of validity for the medium in Hz. + If simulation or plotting functions use frequency out of this range, a warning is thrown. + name : str = None + Optional name for the medium. Example ------- - >>> pole_res = PoleResidue(eps_inf=2.0, poles=[((1,2),(3,4)), ((5,6),(7,8))]) + >>> pole_res = PoleResidue(eps_inf=2.0, poles=[(1+2j, 3+4j), (5+6j, 7+8j)]) + >>> eps = pole_res.eps_model(200e12) """ eps_inf: float = 1.0 @@ -138,34 +321,35 @@ def eps_model(self, frequency: float) -> complex: Parameters ---------- - frequency : ``float`` + frequency : float Frequency to evaluate permittivity at (Hz). Returns ------- - ``complex`` - Complex-valued relative permittivity evaluated at ``frequency``. + complex + Complex-valued relative permittivity evaluated at the frequency. """ omega = 2 * np.pi * frequency eps = self.eps_inf + 0.0j - for p in self.poles: - (ar, ai), (cr, ci) = p - a = ar + 1j * ai - c = cr + 1j * ci + for (a, c) in self.poles: a_cc = np.conj(a) c_cc = np.conj(c) eps -= c / (1j * omega + a) eps -= c_cc / (1j * omega + a_cc) return eps - def __str__(self): - """string representation + @property + def pole_residue(self): + """Representation of Medium as a pole-residue model.""" + return PoleResidue( + eps_inf=self.eps_inf, + poles=self.poles, + frequency_range=self.frequency_range, + name=self.name, + ) - Returns - ------- - TYPE - Description - """ + def __str__(self): + """string representation""" return ( f"td.PoleResidue(" f"\n\tpoles={self.poles}, " @@ -174,7 +358,30 @@ def __str__(self): class Sellmeier(DispersiveMedium): - """Sellmeier model for dispersion""" + """A dispersive medium described by the Sellmeier model. + The frequency-dependence of the refractive index is described by: + + .. math:: + + n(\\lambda)^2 = 1 + \\sum_i \\frac{B_i \\lambda^2}{\\lambda^2 - C_i} + + where :math:`\\lambda` is in microns, :math:`B_i` is unitless and :math:`C_i` is in microns^2. + + Parameters + ---------- + coeffs : List[Tuple[float, float]] + List of Sellmeier (:math:`B_i, C_i`) coefficients. + frequeuncy_range : Tuple[float, float] = (-inf, inf) + Range of validity for the medium in Hz. + If simulation or plotting functions use frequency out of this range, a warning is thrown. + name : str = None + Optional name for the medium. + + Example + ------- + >>> sellmeier_medium = Sellmeier(coeffs=[(1,2), (3,4)]) + >>> eps = sellmeier_medium.eps_model(200e12) + """ coeffs: List[Tuple[float, float]] type: Literal["Sellmeier"] = "Sellmeier" @@ -194,20 +401,64 @@ def eps_model(self, frequency: float) -> complex: Parameters ---------- - frequency : ``float`` + frequency : float Frequency to evaluate permittivity at (Hz). Returns ------- - ``complex`` - Complex-valued relative permittivity evaluated at ``frequency``. + complex + Complex-valued relative permittivity evaluated at the frequency. """ n = self._n_model(frequency) return nk_to_eps_complex(n) + @property + def pole_residue(self): + """Representation of Medium as a pole-residue model.""" + + poles = [] + for (B, C) in self.coeffs: + beta = 2 * np.pi * C_0 / np.sqrt(C) + alpha = -0.5 * beta * B + a = 1j * beta + c = 1j * alpha + poles.append((a, c)) + + return PoleResidue( + eps_inf=1, + poles=poles, + frequency_range=self.frequency_range, + name=self.name, + ) + class Lorentz(DispersiveMedium): - """Lorentz model for dispersion""" + """A dispersive medium described by the Lorentz model. + The frequency-dependence of the complex-valued permittivity is described by: + + .. math:: + \\epsilon(f) = \\epsilon_\\infty + \\sum_i + \\frac{\\Delta\\epsilon_i f_i^2}{f_i^2 + 2jf\\delta_i - f^2} + + where :math:`f, f_i, \\delta_i` are in Hz. + + Parameters + ---------- + eps_inf : float = 1.0 + Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`). + coeffs : List[Tuple[float, float, float]] + List of (:math:`\\Delta\\epsilon_i, f_i, \\delta_i`) values for model. + frequeuncy_range : Tuple[float, float] = (-inf, inf) + Range of validity for the medium in Hz. + If simulation or plotting functions use frequency out of this range, a warning is thrown. + name : str = None + Optional name for the medium. + + Example + ------- + >>> lorentz_medium = Lorentz(eps_inf=2.0, coeffs=[(1,2,3), (4,5,6)]) + >>> eps = lorentz_medium.eps_model(200e12) + """ eps_inf: float = 1.0 coeffs: List[Tuple[float, float, float]] @@ -219,22 +470,74 @@ def eps_model(self, frequency: float) -> complex: Parameters ---------- - frequency : ``float`` + frequency : float Frequency to evaluate permittivity at (Hz). Returns ------- - ``complex`` - Complex-valued relative permittivity evaluated at ``frequency``. + complex + Complex-valued relative permittivity evaluated at the frequency. """ eps = self.eps_inf + 0.0j for (de, f, delta) in self.coeffs: - eps += (de * f ** 2) / (f ** 2 + 2j * f * delta - frequency ** 2) + eps += (de * f ** 2) / (f ** 2 + 2j * frequency * delta - frequency ** 2) return eps + @property + def pole_residue(self): + """Representation of Medium as a pole-residue model.""" + + poles = [] + for (de, f, delta) in self.coeffs: + + w = 2 * np.pi * f + d = 2 * np.pi * delta + + if d > w: + r = 1j * np.sqrt(d * d - w * w) + else: + r = np.sqrt(w * w - d * d) + + a = d - 1j * r + c = 1j * de * w ** 2 / 2 / r + + poles.append((a, c)) + + return PoleResidue( + eps_inf=self.eps_inf, + poles=poles, + frequency_range=self.frequency_range, + name=self.name, + ) + class Debye(DispersiveMedium): - """Debye model for dispersion""" + """A dispersive medium described by the Debye model. + The frequency-dependence of the complex-valued permittivity is described by: + + .. math:: + \\epsilon(f) = \\epsilon_\\infty + \\sum_i + \\frac{\\Delta\\epsilon_i}{1 + jf\\tau_i} + + where :math:`f` is in Hz, and :math:`\\tau_i` is in seconds. + + Parameters + ---------- + eps_inf : float = 1.0 + Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`). + coeffs : List[Tuple[float, float, float]] + List of (:math:`\\Delta\\epsilon_i, \\tau_i`) values for model. + frequeuncy_range : Tuple[float, float] = (-inf, inf) + Range of validity for the medium in Hz. + If simulation or plotting functions use frequency out of this range, a warning is thrown. + name : str = None + Optional name for the medium. + + Example + ------- + >>> debye_medium = Debye(eps_inf=2.0, coeffs=[(1,2),(3,4)]) + >>> eps = debye_medium.eps_model(200e12) + """ eps_inf: float = 1.0 coeffs: List[Tuple[float, float]] @@ -246,23 +549,41 @@ def eps_model(self, frequency: float) -> complex: Parameters ---------- - frequency : ``float`` + frequency : float Frequency to evaluate permittivity at (Hz). Returns ------- - ``complex`` - Complex-valued relative permittivity evaluated at ``frequency``. + complex + Complex-valued relative permittivity evaluated at the frequency. """ eps = self.eps_inf + 0.0j for (de, tau) in self.coeffs: eps += de / (1 + 1j * frequency * tau) return eps + @property + def pole_residue(self): + """Representation of Medium as a pole-residue model.""" + + poles = [] + for (de, tau) in self.coeffs: + a = 2 * np.pi / tau + 0j + c = -0.5 * de * a + poles.append((a, c)) + + return PoleResidue( + eps_inf=self.eps_inf, + poles=poles, + frequency_range=self.frequency_range, + name=self.name, + ) -MediumType = Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye] -""" conversion helpers """ +# types of mediums that can be used in Simulation and Structures +MediumType = Union[Literal[PEC], Medium, AnisotropicMedium, PoleResidue, Sellmeier, Lorentz, Debye] + +""" Conversion helper functions """ def nk_to_eps_complex(n: float, k: float = 0.0) -> complex: @@ -270,14 +591,14 @@ def nk_to_eps_complex(n: float, k: float = 0.0) -> complex: Parameters ---------- - n : ``float`` - real part of refractive index - k : ``float`` = 0 - imaginary part of refrative index. + n : float + Real part of refractive index. + k : float = 0.0 + Imaginary part of refrative index. Returns ------- - ``complex`` + complex Complex-valued relative permittivty. """ eps_real = n ** 2 - k ** 2 @@ -286,16 +607,16 @@ def nk_to_eps_complex(n: float, k: float = 0.0) -> complex: def eps_complex_to_nk(eps_c: complex) -> Tuple[float, float]: - """convert complex permittivity to n, k + """Convert complex permittivity to n, k values. Parameters ---------- - eps_c : ``complex`` + eps_c : complex Complex-valued relative permittivity. Returns ------- - ``(float, float)`` + Tuple[float, float] Real and imaginary parts of refractive index (n & k). """ ref_index = np.sqrt(eps_c) @@ -303,21 +624,21 @@ def eps_complex_to_nk(eps_c: complex) -> Tuple[float, float]: def nk_to_eps_sigma(n: float, k: float, freq: float) -> Tuple[float, float]: - """convert n, k at freq to permittivity and conductivity + """Convert ``n``, ``k`` at frequency ``freq`` to permittivity and conductivity values. Parameters ---------- - n : ``float`` - real part of refractive index - k : ``float`` = 0 - imaginary part of refrative index. - frequency : ``float`` + n : float + Real part of refractive index. + k : float = 0.0 + Imaginary part of refrative index. + frequency : float Frequency to evaluate permittivity at (Hz). Returns ------- - ``(float, float)`` - Real part of relative permittivity & conductivity. + Tuple[float, float] + Real part of relative permittivity & electric conductivity. """ eps_complex = nk_to_eps_complex(n, k) eps_real, eps_imag = eps_complex.real, eps_complex.imag @@ -327,15 +648,15 @@ def nk_to_eps_sigma(n: float, k: float, freq: float) -> Tuple[float, float]: def nk_to_medium(n: float, k: float, freq: float) -> Medium: - """Convert ``n`` and ``k`` values at ``frequency`` to :class:`Medium`. + """Convert ``n`` and ``k`` values at frequency ``freq`` to :class:`Medium`. Parameters ---------- - n : ``float`` - real part of refractive index - k : ``float`` = 0 - imaginary part of refrative index. - frequency : ``float`` + n : float + Real part of refractive index. + k : float = 0 + Imaginary part of refrative index. + frequency : float Frequency to evaluate permittivity at (Hz). Returns @@ -352,11 +673,11 @@ def eps_sigma_to_eps_complex(eps_real: float, sigma: float, freq: float) -> comp Parameters ---------- - eps_real : ``float`` + eps_real : float Real-valued relative permittivity. - sigma : ``float`` + sigma : float Conductivity. - freq : ``float`` + freq : float Frequency to evaluate permittivity at (Hz). Returns diff --git a/tidy3d/components/mode.py b/tidy3d/components/mode.py index 4247b8a0fd..aa87b1ed3e 100644 --- a/tidy3d/components/mode.py +++ b/tidy3d/components/mode.py @@ -6,24 +6,50 @@ from .base import Tidy3dBaseModel from .types import Symmetry +from ..log import SetupError class Mode(Tidy3dBaseModel): - """Stores Specifications of a Mode to input into mode solver. + """Stores specifications for the mode solver to find an electromagntic mode. + Note, the planar axes are found by popping the propagation axis from {x,y,z}. + For example, if propagation axis is y, the planar axes are ordered {x,z}. + Parameters ---------- - mode_index : ``int`` + mode_index : int Return the mode solver output at ``mode_index``. - target_neff : ``float = None`` + Must be >= 0. + num_modes : int = None + Number of modes returned by mode solver before selecting mode at ``mode_index``. + Must be > ``mode_index`` to accomodate ``mode_index``-th mode. + target_neff : float = None Guess for effective index of mode. - symmetries : ``(int, int) = (0,0)`` - Symmetries (0, 1,-1) = (none, even, odd) in (x,y) of mode plane. - num_pml: ``(int, int) = (0,0)`` - number of standard pml layers to add in (x,y) of mode plane. + Must be > 0. + symmetries : Tuple[int, int] = (0,0) + Symmetries to apply to mode solver for first two non-propagation axes. + Values of (0, 1,-1) correspond to (none, even, odd) symmetries, respectvely. + num_pml: Tuple[int, int] = (0,0) + Number of standard pml layers to add in the first two non-propagation axes. + + Example + ------- + >>> mode = Mode(mode_index=1, num_modes=3, target_neff=1.5, symmetries=(1,-1)) """ mode_index: pd.NonNegativeInt num_modes: pd.PositiveInt = None - target_neff: float = None + target_neff: pd.PositiveFloat = None symmetries: Tuple[Symmetry, Symmetry] = (0, 0) num_pml: Tuple[pd.NonNegativeInt, pd.NonNegativeInt] = (0, 0) + + @pd.validator("num_modes", always=True) + def check_num_modes(cls, val, values): + """Make sure num_modes is > mode_index or None""" + if val is not None: + mode_index = values.get("mode_index") + if not val > mode_index: + raise SetupError( + "`num_modes` must be greater than `mode_index`" + f"given {val} and {mode_index}, respectively" + ) + return val diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index a27f0499e2..d60ee1e06c 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -1,4 +1,4 @@ -""" Objects that define how data is recorded from simulation """ +"""Objects that define how data is recorded from simulation.""" from abc import ABC from typing import List, Union @@ -11,54 +11,64 @@ from .viz import add_ax_if_none, MonitorParams from ..log import SetupError -""" Monitors """ - class Monitor(Box, ABC): - """base class for monitors, which all have Box shape""" + """Abstract base class for monitors.""" name: str _name_validator = validate_name_str() @add_ax_if_none - def plot( + def plot( #pylint:disable=duplicate-code self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: - """plot monitor geometry""" + """Plot the monitor geometry on a cross section plane. + + Parameters + ---------- + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **patch_kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. #pylint:disable=line-too-long # pylint: disable=line-too-long + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ kwargs = MonitorParams().update_params(**kwargs) ax = self.geometry.plot(x=x, y=y, z=z, ax=ax, **kwargs) return ax @property def geometry(self): - """box representation of self""" - return Box(center=self.center, size=self.size) - + """:class:`Box` representation of monitor. -""" The following are abstract classes that separate the ``Monitor`` instances into different - types depending on what they store. - They can be useful for keeping argument types and validations separated. - For example, monitors that should always be defined on planar geometries can have an - ``_assert_plane()`` validation in the abstract base class ``PlanarMonitor``. - This way, ``_assert_plane()`` will always be used if we add more ``PlanarMonitor`` objects in - the future. - This organization is also useful when doing conditions based on monitor / data type. - For example, instead of - ``if isinstance(mon_data, (FieldMonitor, FieldTimeMonitor)):`` we can simply do - ``if isinstance(mon_data, ScalarFieldMonitor)`` and this will generalize if we add more - ``ScalarFieldMonitor`` objects in the future. -""" + Returns + ------- + :class:`Box` + Representation of the monitor geometry as a :class:`Box`. + """ + return Box(center=self.center, size=self.size) class FreqMonitor(Monitor, ABC): - """stores data in frequency domain""" + """Stores data in the frequency-domain.""" freqs: Union[List[float], Array[float]] class TimeMonitor(Monitor, ABC): - """stores data in time domain""" + """Stores data in the time-domain.""" start: pydantic.NonNegativeFloat = 0.0 stop: pydantic.NonNegativeFloat = None @@ -66,7 +76,7 @@ class TimeMonitor(Monitor, ABC): @pydantic.validator("stop", always=True) def stop_greater_than_start(cls, val, values): - """make sure stop is greater than or equal to start""" + """Ensure sure stop is greater than or equal to start.""" start = values.get("start") if val and val < start: raise SetupError("Monitor start time is greater than stop time.") @@ -74,13 +84,13 @@ def stop_greater_than_start(cls, val, values): class AbstractFieldMonitor(Monitor, ABC): - """stores data as a function of x,y,z""" + """Stores electromagnetic field data as a function of x,y,z.""" fields: List[EMField] = ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"] class PlanarMonitor(Monitor, ABC): - """stores quantities on a plane""" + """Monitors that must have planar geometry.""" _plane_validator = assert_plane() @@ -89,24 +99,31 @@ class AbstractFluxMonitor(PlanarMonitor, ABC): """stores flux through a plane""" -""" usable """ - - class FieldMonitor(AbstractFieldMonitor, FreqMonitor): - """Stores EM fields or permittivity as a function of frequency. + """Stores a collection of electromagnetic fields in the frequency domain. Parameters ---------- - center: ``(float, float, float)``, optional. - Center of monitor ``Box``, defaults to (0, 0, 0) - size: ``(float, float, float)`` - Size of monitor ``Box``, must have one element = 0.0 to define plane. - fields: ``[str]``, optional - Electromagnetic field(s) to measure (E, H), defaults to ``['Ex', 'Ey', 'Ez', 'Hx', 'Hy', - 'Hz']``. - freqs: ``[float]`` - Frequencies to measure fields at at (Hz), - + center: Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of monitor. + size: Tuple[float, float, float] + Size of monitor. + All elements must be non-negative. + fields: List[str] = ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'] + Specifies the electromagnetic field components to record. + If wanting to conserve data, can specify fewer components. + freqs: List[float] or np.ndarray + List of frequencies in Hertz to store fields at. + name : str + (Required) name used to access data after simulation is finished. + + Example + ------- + >>> monitor = FieldMonitor( + ... size=(2,2,2), + ... freqs=[200e12, 210e12], + ... fields=['Ex', 'Ey', 'Hz'], + ... name='freq_domain_fields') """ fields: List[FieldType] = ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"] @@ -115,23 +132,40 @@ class FieldMonitor(AbstractFieldMonitor, FreqMonitor): class FieldTimeMonitor(AbstractFieldMonitor, TimeMonitor): - """Stores EM fields as a function of time. + """Stores a collection of electromagnetic fields in the time domain. Parameters ---------- - center: Tuple[float, float, float], optional. - Center of monitor ``Box``, defaults to (0, 0, 0) - size: Tuple[float, float, float]. - Size of monitor ``Box``, must have one element = 0.0 to define plane. - fields: List[str], optional - Electromagnetic field(s) to measure (E, H), defaults to ``['Ex', 'Ey', 'Ez', 'Hx', 'Hy', - 'Hz']``. - start: ``float = 0.0`` - (seconds) Time to start monitoring fields. - stop: ``float = None`` - (seconds) Time to stop monitoring fields, end of simulation if not specified. - interval: ``int = 1`` - Records data at every ``interval`` time steps in the simulation. + center: Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of monitor. + size: Tuple[float, float, float] + Size of monitor. + All elements must be non-negative. + fields: List[str] = ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'] + Specifies the electromagnetic field components to record. + If wanting to conserve data, can specify fewer components. + start : float = 0.0 + Time (seconds) to start recording fields. + stop : float = None + Time (seconds) to stop recording fields. + Must be greater than or equal to ``start``. + If not specified, records until the end of the simulation. + interval : int = 1 + Number of time steps between measurements. + To conserve data, intervals > 1 may be specified to record data more sparsely sampled data. + Must be positive. + name : str + (Required) name used to access data after simulation is finished. + + Example + ------- + >>> monitor = FieldTimeMonitor( + ... size=(2,2,2), + ... fields=['Hx'], + ... start=1e-13, + ... stop=5e-13, + ... interval=2, + ... name='movie_monitor') """ type: Literal["FieldTimeMonitor"] = "FieldTimeMonitor" @@ -143,12 +177,20 @@ class FluxMonitor(AbstractFluxMonitor, FreqMonitor): Parameters ---------- - center: Tuple[float, float, float], optional. - Center of monitor ``Box``, defaults to (0, 0, 0) - size: Tuple[float, float, float]. - Size of monitor ``Box``, must have one element = 0.0 to define plane. - freqs: List[float] - Frequencies to measure flux at. + center: Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of monitor. + size: Tuple[float, float, float] + Size of monitor. + All elements must be non-negative. + One element must be 0.0 to define flux plane. + freqs: List[float] or np.ndarray + List of frequencies in Hertz to store fields at. + name : str + (Required) name used to access data after simulation is finished. + + Example + ------- + >>> monitor = FluxMonitor(size=(2,2,0), freqs=[200e12, 210e12], name='flux_monitor') """ type: Literal["FluxMonitor"] = "FluxMonitor" @@ -156,20 +198,37 @@ class FluxMonitor(AbstractFluxMonitor, FreqMonitor): class FluxTimeMonitor(AbstractFluxMonitor, TimeMonitor): - """Stores power flux through a plane as a function of frequency. + """Stores power flux through a plane as a function of time. Parameters ---------- - center: Tuple[float, float, float], optional. - Center of monitor ``Box``, defaults to (0, 0, 0) - size: Tuple[float, float, float]. - Size of monitor ``Box``, must have one element = 0.0 to define plane. - start: ``float = 0.0`` - (seconds) Time to start monitoring flux. - stop: ``float = None`` - (seconds) Time to stop monitoring flux, end of simulation if not specified. - interval: ``int = 1`` - Records data at every ``interval`` time steps in the simulation. + center: Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of monitor. + size: Tuple[float, float, float] + Size of monitor. + All elements must be non-negative. + One element must be 0.0 to define flux plane. + start : float = 0.0 + Time (seconds) to start recording fields. + stop : float = None + Time (seconds) to stop recording fields. + Must be greater than or equal to ``start``. + If not specified, records until the end of the simulation. + interval : int = 1 + Number of time steps between measurements. + To conserve data, intervals > 1 may be specified to record data more sparsely sampled data. + Must be positive. + name : str + (Required) name used to access data after simulation is finished. + + Example + ------- + >>> monitor = FluxTimeMonitor( + ... size=(2,2,0), + ... start=1e-13, + ... stop=5e-13, + ... interval=2, + ... name='flux_time') """ type: Literal["FluxTimeMonitor"] = "FluxTimeMonitor" @@ -177,18 +236,31 @@ class FluxTimeMonitor(AbstractFluxMonitor, TimeMonitor): class ModeMonitor(PlanarMonitor, FreqMonitor): - """stores overlap amplitudes associated with modes. + """Stores amplitudes found through modal decomposition of fields on plane. Parameters ---------- - center: Tuple[float, float, float], optional. - Center of monitor ``Box``, defaults to (0, 0, 0) - size: Tuple[float, float, float]. - Size of monitor ``Box``, must have one element = 0.0 to define plane. - freqs: List[float] - Frequencies to measure flux at. - modes: List[``Mode``] - List of ``Mode`` objects specifying the modal profiles to measure amplitude overalap with. + center: Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of monitor. + size: Tuple[float, float, float] + Size of monitor. + All elements must be non-negative. + One element must be 0.0 to define mode plane. + freqs: List[float] or np.ndarray + List of frequencies in Hertz to compute the modal decomposition on. + modes : List[:class:`Mode`] + List of mode specifications to compute modal overlaps with. + name : str + (Required) name used to access data after simulation is finished. + + Example + ------- + >>> modes = [Mode(mode_index=0), Mode(mode_index=1)] + >>> monitor = ModeMonitor( + ... size=(2,2,0), + ... freqs=[200e12, 210e12], + ... modes=modes, + ... name='mode_monitor') """ direction: List[Direction] = ["+", "-"] @@ -197,14 +269,5 @@ class ModeMonitor(PlanarMonitor, FreqMonitor): data_type: Literal["ModeData"] = "ModeData" -""" explanation of monitor_type_map: - When we load monitor data from file, we need some way to know what type of ``Monitor`` created - the data. - The ``Monitor``'s' ``type`` itself is not serilizable, so we can't store that directly in json. - However, the ``Montior.type`` attribute stores a string representation of the ``MonitorType``, - so we can use that. - This map allows one to recover the ``Monitor`` type from the ``.type`` attribute in the json - object and therefore load the correct monitor. -""" - +# types of monitors that are accepted by simulation MonitorType = Union[FieldMonitor, FieldTimeMonitor, FluxMonitor, FluxTimeMonitor, ModeMonitor] diff --git a/tidy3d/components/pml.py b/tidy3d/components/pml.py index 345291c395..ecfe559710 100644 --- a/tidy3d/components/pml.py +++ b/tidy3d/components/pml.py @@ -1,36 +1,98 @@ -""" Defines profile of Perfectly-matched layers (absorber) """ +"""Defines profile of Perfectly-matched layers (absorber)""" from typing import Union, Literal +from abc import ABC import pydantic from .base import Tidy3dBaseModel -"""TODO: better docstrings.""" +# TODO: More explanation on parameters, when to use various PMLs. class AbsorberParams(Tidy3dBaseModel): - """Specifies parameters for an Absorber or PML. Sigma is in units of 2*EPSILON_0/dt.""" + """Specifies parameters common to Absorbers and PMLs. - sigma_order: pydantic.NonNegativeInt - sigma_min: pydantic.NonNegativeFloat - sigma_max: pydantic.NonNegativeFloat + Parameters + ---------- + sigma_order : int = 3 + Order of the polynomial describing the absorber profile (~dist^sigma_order). + Must be non-negative. + sigma_min : float = 0.0 + Minimum value of the absorber conductivity. + Units of 2*EPSILON_0/dt. + Must be non non-negative. + sigma_max : float = 1.5 + Maximum value of the absorber conductivity. + Units of 2*EPSILON_0/dt. + Must be non non-negative. + + Example + ------- + >>> params = AbsorberParams(sigma_order=3, sigma_min=0.0, sigma_max=1.5) + """ + + sigma_order: pydantic.NonNegativeInt = 3 + sigma_min: pydantic.NonNegativeFloat = 0.0 + sigma_max: pydantic.NonNegativeFloat = 1.5 class PMLParams(AbsorberParams): - """Extra parameters needed for complex frequency-shifted PML. Kappa is dimensionless, alpha - is in the same units as sigma.""" + """Specifies full set of parameters needed for complex, frequency-shifted PML. + + Parameters + ---------- + sigma_order : int = 3 + Order of the polynomial describing the absorber profile (sigma~dist^sigma_order). + Must be non-negative. + sigma_min : float = 0.0 + Minimum value of the absorber conductivity. + Units of 2*EPSILON_0/dt. + Must be non-negative. + sigma_max : float = 1.5 + Maximum value of the absorber conductivity. + Units of 2*EPSILON_0/dt. + Must be non-negative. + kappa_order : int = 3 + Order of the polynomial describing the PML kappa profile (kappa~dist^kappa_order). + Must be non-negative. + kappa_min : float = 0.0 + Minimum value of the PML kappa. + Dimensionless. + Must be non-negative. + kappa_max : float = 1.5 + Maximum value of the PML kappa. + Dimensionless. + Must be non-negative. + alpha_order : int = 3 + Order of the polynomial describing the PML alpha profile (alpha~dist^alpha_order). + Must be non-negative. + alpha_min : float = 0.0 + Minimum value of the PML alpha. + Units of 2*EPSILON_0/dt. + Must be non-negative. + alpha_max : float = 1.5 + Maximum value of the PML alpha. + Units of 2*EPSILON_0/dt. + Must be non-negative. + + Example + ------- + >>> params = PMLParams(sigma_order=3, sigma_min=0.0, sigma_max=1.5, kappa_min=0.0) + """ + + kappa_order: pydantic.NonNegativeInt = 3 + kappa_min: pydantic.NonNegativeFloat = 0.0 + kappa_max: pydantic.NonNegativeFloat = 1.5 + alpha_order: pydantic.NonNegativeInt = 3 + alpha_min: pydantic.NonNegativeFloat = 0.0 + alpha_max: pydantic.NonNegativeFloat = 1.5 - kappa_order: pydantic.NonNegativeInt - kappa_min: pydantic.NonNegativeFloat - kappa_max: pydantic.NonNegativeFloat - alpha_order: pydantic.NonNegativeInt - alpha_min: pydantic.NonNegativeFloat - alpha_max: pydantic.NonNegativeFloat +""" Default parameters """ -AbsorberPs = AbsorberParams(sigma_order=3, sigma_min=0.0, sigma_max=6.4) -StandardPs = PMLParams( +DefaultAbsorberParameters = AbsorberParams(sigma_order=3, sigma_min=0.0, sigma_max=6.4) +DefaultPMLParameters = PMLParams( sigma_order=3, sigma_min=0.0, sigma_max=1.5, @@ -41,7 +103,7 @@ class PMLParams(AbsorberParams): alpha_min=0.0, alpha_max=0.0, ) -StablePs = PMLParams( +DefaultStablePMLParameters = PMLParams( sigma_order=3, sigma_min=0.0, sigma_max=1.0, @@ -53,11 +115,24 @@ class PMLParams(AbsorberParams): alpha_max=0.9, ) +""" PML specifications """ -class AbsorberSpec(Tidy3dBaseModel): - """Specifies the absorber along a single dimension.""" + +class AbsorberSpec(Tidy3dBaseModel, ABC): + """Abstract base class. + Specifies the generic absorber properties along a single dimension. + + Parameters + ---------- + num_layers : int = 12 + Number of layers of standard PML to add to + and - boundaries. + Must be non-negative. + parameters : :class:`AbsorberParams` + Parameters to fine tune the absorber profile and properties. + """ num_layers: pydantic.NonNegativeInt + parameters: AbsorberParams class PML(AbsorberSpec): @@ -65,9 +140,10 @@ class PML(AbsorberSpec): Parameters ---------- - num_layers : ``int``, optional - Number of layers of PML to add to + and - boundaries, default = 12. - pml_params : :class:PMLParams + num_layers : int = 12 + Number of layers of standard PML to add to + and - boundaries. + Must be non-negative. + parameters : :class:`PMLParams` = DefaultPMLParameters Parameters of the complex frequency-shifted absorption poles. Example @@ -76,41 +152,50 @@ class PML(AbsorberSpec): """ num_layers: pydantic.NonNegativeInt = 12 - parameters: PMLParams = StandardPs + parameters: PMLParams = DefaultPMLParameters class StablePML(AbsorberSpec): """Specifies a 'stable' PML along a single dimension. + This PML deals handles possbly divergent simulations better, but at the expense of more layers. Parameters ---------- - num_layers : ``int``, optional - Number of layers of PML to add to + and - boundaries, default = 40. + num_layers : int = 40 + Number of layers of stable PML to add to + and - boundaries. + Must be non-negative. + parameters : Literal[DefaultStablePMLParameters] = DefaultStablePMLParameters + "Stable" parameters of the complex frequency-shifted absorption poles. Example ------- - >>> pml = StablePML(num_layers=100) + >>> pml = StablePML(num_layers=40) """ num_layers: pydantic.NonNegativeInt = 40 - parameters: Literal[StablePs] = StablePs + parameters: Literal[DefaultStablePMLParameters] = DefaultStablePMLParameters class Absorber(AbsorberSpec): - """Specifies an adiab absorber along a single dimension. + """Specifies an adiabatic absorber along a single dimension. + This absorber is well-suited for dispersive materials + intersecting with absorbing edges of the simulation at the expense of more layers. Parameters ---------- - num_layers : ``int``, optional - Number of layers of PML to add to + and - boundaries, default = 40. + num_layers : int = 40 + Number of layers of absorber to add to + and - boundaries. + parameters : :class:`AbsorberParams` = DefaultAbsorberParameters + General absorber parameters. Example ------- - >>> pml = Absorber(num_layers=100) + >>> pml = Absorber(num_layers=40) """ num_layers: pydantic.NonNegativeInt = 40 - parameters: AbsorberParams = AbsorberPs + parameters: AbsorberParams = DefaultAbsorberParameters +# pml types allowed in simulation init PMLTypes = Union[PML, StablePML, Absorber, None] diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 699ab38e4c..d44793e5d1 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -1,4 +1,3 @@ -# pylint: disable=unused-import """ Container holding all information about simulation and its components""" from typing import Dict, Tuple, List @@ -10,31 +9,34 @@ from mpl_toolkits.axes_grid1 import make_axes_locatable from descartes import PolygonPatch -from .types import Symmetry, Ax, Shapely, FreqBound from .validators import assert_unique_names, assert_objects_in_sim_bounds, set_names -from .geometry import Box, PolySlab, Cylinder, Sphere -from .types import Symmetry, Ax, Shapely, FreqBound, Numpy from .geometry import Box +from .types import Symmetry, Ax, Shapely, FreqBound from .grid import Coords1D, Grid, Coords from .medium import Medium, MediumType, eps_complex_to_nk from .structure import Structure -from .source import SourceType, VolumeSource, GaussianPulse -from .monitor import MonitorType, FieldMonitor, FluxMonitor +from .source import SourceType +from .monitor import MonitorType from .pml import PMLTypes, PML from .viz import StructMediumParams, StructEpsParams, PMLParams, SymParams, add_ax_if_none from ..constants import inf, C_0 -from ..log import log, SetupError +from ..log import log + +# for docstring examples +from .geometry import Sphere, Cylinder, PolySlab # pylint:disable=unused-import +from .source import VolumeSource, GaussianPulse # pylint:disable=unused-import +from .monitor import FieldMonitor, FluxMonitor # pylint:disable=unused-import # technically this is creating a circular import issue because it calls tidy3d/__init__.py # from .. import __version__ as version_number class Simulation(Box): # pylint:disable=too-many-public-methods - """Contains all information about simulation. + """Contains all information about Tidy3d simulation. Parameters ---------- - center : Tuple[float, float, float] = ``(0.0, 0.0, 0.0)`` + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) (microns) Center of simulation domain in x, y, and z. size : Tuple[float, float, float] (microns) Size of simulation domain in x, y, and z. @@ -42,35 +44,42 @@ class Simulation(Box): # pylint:disable=too-many-public-methods grid_size : Tuple[float, float, float] (microns) Grid size along x, y, and z. Each element must be non-negative. - run_time : float = ``0.0`` - (seconds) Maximum run time of simulation. - If ``shutoff`` specified, simulation will terminate early when shutoff condition met. + run_time : float = 0.0 + Total electromagnetic evolution time in seconds. + Note: If ``shutoff`` specified, simulation will terminate early when shutoff condition met. Must be non-negative. - medium : :class:`Medium` = ``Medium(permittivity=1.0)`` + medium : :class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye` = ``Medium(permittivity=1.0)`` Background :class:`tidy3d.Medium` of simulation, defaults to air. - structures : List[:class:`Structure`] = ``{}`` - Structures in simulation. - Structures defined later in this list override the simulation material properties in + structures : List[:class:`Structure`] = [] + List of structures in simulation. + Note: Structures defined later in this list override the simulation material properties in regions of spatial overlap. - sources : List[:class:`Source`] = ``[]`` - Named mapping of electric current sources in the simulation. - monitors : List[:class:`Monitor`] = ``[]`` - Named mapping of field and data monitors in the simulation. + sources : List[:class:`VolumeSource` or :class:`PlaneWave` or :class:`ModeSource`] = [] + List of electric current sources injecting fields into the simulation. + monitors : List[:class:`FieldMonitor` or :class:`FieldTimeMonitor` or :class:`FluxMonitor` or :class:`FluxTimeMonitor` or :class:`ModeMonitor`] = [] + List of monitors in the simulation. + Note: names stored in ``monitor.name`` are used to access data after simulation is run. pml_layers : Tuple[:class:`AbsorberSpec`, :class:`AbsorberSpec`, :class:`AbsorberSpec`] = ``(None, None, None)`` Specifications for the absorbing layers on x, y, and z edges. Elements of ``None`` are assumed to have no absorber and use periodic boundary conditions. - symmetry : Tuple[int, int, int] = ``(0, 0, 0)`` - Specifies symmetry in x, y, and z dimensions. - Only values of 0, 1, and -1 are accepted and specify no symmetry, even symmetry, and - odd symmetry, respectively. - shutoff : float = ``1e-5`` - Value of the average intensity in the simulation relative to the maximum at which the - simulation terminates. - subpixel : bool = ``True`` + symmetry : Tuple[int, int, int] = (0, 0, 0) + Tuple of integers defining reflection symmetry across a + plane bisecting the simulation domain normal to the x-, y-, and + z-axis, respectively. Each element can be ``0`` (no symmetry), + ``1`` (even, i.e. 'PMC' symmetry) or ``-1`` (odd, i.e. 'PEC' + symmetry). + Note that the vectorial nature of the fields must be taken into account to correctly + determine the symmetry value. + shutoff : float = 1e-5 + Ratio of the instantaneous integrated E-field intensity to the maximum value + at which the simulation will automatically shut down. + Used to prevent extraneous run time of simulations with fully decayed fields. + Set to ``0`` to disable this feature. + subpixel : bool = True If ``True``, uses subpixel averaging of the permittivity based on structure definition, resulting in much higher accuracy for a given grid size. - courant : float = ``0.9`` + courant : float = 0.9 Courant stability factor, controls time step to spatial step ratio. Lower values lead to more stable simulations for dispersive materials, but result in longer simulation times. @@ -173,51 +182,77 @@ def set_medium_names(cls, val, values): # make sure all names are unique _unique_structure_names = assert_unique_names("structures") - _unique_medium_names = assert_unique_names("structures", check_mediums=True) _unique_source_names = assert_unique_names("sources") _unique_monitor_names = assert_unique_names("monitors") + # _unique_medium_names = assert_unique_names("structures", check_mediums=True) # TODO: # - check sources in medium freq range # - check PW in homogeneous medium # - check nonuniform grid covers the whole simulation domain + # - check any structures close to PML (in lambda) without intersecting. """ Accounting """ @property - def medium_map(self) -> Dict[Medium, pydantic.NonNegativeInt]: - """``medium_map[medium]`` returns unique global index of :class:`Medium` in simulation. + def mediums(self) -> List[MediumType]: + """Returns set of distinct :class:`AbstractMedium` in simulation. + + Returns + ------- + Set[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`] + Set of distinct mediums in the simulation. + """ + return {structure.medium for structure in self.structures} + + @property + def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: + """Returns dict mapping medium to index in material. + ``medium_map[medium]`` returns unique global index of :class:`AbstractMedium` in simulation. Returns ------- - {:class:`Medium`, ``int``} - Mapping between a :class:`Medium` and it's index in the simulation. + Dict[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`, int] + Mapping between distinct mediums to index in simulation. """ - mediums = {structure.medium for structure in self.structures} - return {medium: index for index, medium in enumerate(mediums)} + return {medium: index for index, medium in enumerate(self.mediums)} """ Plotting """ @add_ax_if_none - def plot( - self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs + def plot( # pylint:disable=too-many-arguments + self, + x: float = None, + y: float = None, + z: float = None, + grid_lines: bool = False, + ax: Ax = None, + **kwargs, ) -> Ax: - """Plot each of simulation's components on a plan defined by one nonzero x,y,z - coordinate. + """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. Parameters ---------- - x : ``float`` - Position of point in x direction. - y : ``float`` - Position of point in y direction. - z : ``float`` - Position of point in z direction. - ax : Ax, optional - Description + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + grid_lines : bool = False + If true, displays FDTD cell boundaries on plot. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. **kwargs - Description + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. """ ax = self.plot_structures(ax=ax, x=x, y=y, z=z, **kwargs) @@ -225,7 +260,9 @@ def plot( ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_pml(ax=ax, x=x, y=y, z=z, **kwargs) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + if grid_lines: + ax = self.plot_cells(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @add_ax_if_none @@ -235,11 +272,37 @@ def plot_eps( # pylint: disable=too-many-arguments y: float = None, z: float = None, freq: float = None, + grid_lines: bool = False, ax: Ax = None, **kwargs, ) -> Ax: - """Plot each of simulation's components on a plane where structures permittivities are - plotted in grayscale. + """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. + The permittivity is plotted in grayscale based on its value at the specified frequency. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + freq : float = None + Frequency to evaluate the relative permittivity of all mediums. + If not specified, evaluates at infinite frequency. + grid_lines : bool = False + If true, displays FDTD cell boundaries on plot. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. """ ax = self.plot_structures_eps(freq=freq, cbar=True, ax=ax, x=x, y=y, z=z, **kwargs) @@ -247,14 +310,37 @@ def plot_eps( # pylint: disable=too-many-arguments ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_pml(ax=ax, x=x, y=y, z=z, **kwargs) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + if grid_lines: + ax = self.plot_cells(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @add_ax_if_none def plot_structures( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: - """plot all of simulation's structures as distinct materials.""" + """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ medium_map = self.medium_map medium_shapes = self._filter_plot_structures(x=x, y=y, z=z) for (medium, shape) in medium_shapes: @@ -265,12 +351,12 @@ def plot_structures( kwargs_struct["facecolor"] = "white" patch = PolygonPatch(shape, **kwargs_struct) ax.add_artist(patch) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @staticmethod def _add_cbar(eps_min: float, eps_max: float, ax: Ax = None) -> None: - """add colorbar to eps plot""" + """Add a colorbar to eps plot.""" norm = mpl.colors.Normalize(vmin=eps_min, vmax=eps_max) divider = make_axes_locatable(ax) cax = divider.append_axes("right", size="5%", pad=0.15) @@ -288,7 +374,32 @@ def plot_structures_eps( # pylint: disable=too-many-arguments,too-many-locals ax: Ax = None, **kwargs, ) -> Ax: - """Plots all of simulation's structures as permittivity grayscale.""" + """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. + The permittivity is plotted in grayscale based on its value at the specified frequency. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + freq : float = None + Frequency to evaluate the relative permittivity of all mediums. + If not specified, evaluates at infinite frequency. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ if freq is None: freq = inf eps_list = [s.medium.eps_model(freq).real for s in self.structures] @@ -305,36 +416,99 @@ def plot_structures_eps( # pylint: disable=too-many-arguments,too-many-locals ax.add_artist(patch) if cbar: self._add_cbar(eps_min=eps_min, eps_max=eps_max, ax=ax) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @add_ax_if_none def plot_sources( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: - """Plots each of simulation's sources on plane.""" + """Plot each of simulation's sources on a plane defined by one nonzero x,y,z coordinate. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ for source in self.sources: if source.intersects_plane(x=x, y=y, z=z): ax = source.plot(ax=ax, x=x, y=y, z=z, **kwargs) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @add_ax_if_none def plot_monitors( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: - """Plots each of simulation's monitors on plane.""" + """Plot each of simulation's monitors on a plane defined by one nonzero x,y,z coordinate. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ for monitor in self.monitors: if monitor.intersects_plane(x=x, y=y, z=z): ax = monitor.plot(ax=ax, x=x, y=y, z=z, **kwargs) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @add_ax_if_none def plot_symmetries( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: - """plots each of the non-zero symmetries""" + """Plot each of simulation's symmetries on a plane defined by one nonzero x,y,z coordinate. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ for sym_axis, sym_value in enumerate(self.symmetry): if sym_value == 0: continue @@ -346,13 +520,19 @@ def plot_symmetries( if sym_box.intersects_plane(x=x, y=y, z=z): new_kwargs = SymParams(sym_value=sym_value).update_params(**kwargs) ax = sym_box.plot(ax=ax, x=x, y=y, z=z, **new_kwargs) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @property def num_pml_layers(self) -> List[Tuple[float, float]]: - """Number of PML layers in all three axes and directions (-, +).""" + """Number of absorbing layers in all three axes and directions (-, +). + + Returns + ------- + List[Tuple[float, float]] + List containing the number of absorber layers in - and + boundaries. + """ num_layers = [] for pml_axis, pml_layer in enumerate(self.pml_layers): if self.symmetry[pml_axis] != 0: @@ -363,12 +543,18 @@ def num_pml_layers(self) -> List[Tuple[float, float]]: @property def pml_thicknesses(self) -> List[Tuple[float, float]]: - """Thicknesses (um) of PML in all three axes and directions (-, +)""" + """Thicknesses (um) of absorbers in all three axes and directions (-, +) + + Returns + ------- + List[Tuple[float, float]] + List containing the absorber thickness (micron) in - and + boundaries. + """ num_layers = self.num_pml_layers pml_thicknesses = [] - for boundaries in self.grid.boundaries.dict().values(): - thick_l = boundaries[num_layers[0]] - boundaries[0] - thick_r = boundaries[-1] - boundaries[-1 - num_layers[1]] + for num_layer, boundaries in zip(num_layers, self.grid.boundaries.dict().values()): + thick_l = boundaries[num_layer[0]] - boundaries[0] + thick_r = boundaries[-1] - boundaries[-1 - num_layer[1]] pml_thicknesses.append((thick_l, thick_r)) return pml_thicknesses @@ -376,7 +562,29 @@ def pml_thicknesses(self) -> List[Tuple[float, float]]: def plot_pml( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: - """plots each of simulation's PML regions""" + """Plot each of simulation's absorbing boundaries + on a plane defined by one nonzero x,y,z coordinate. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ kwargs = PMLParams().update_params(**kwargs) pml_thicks = self.pml_thicknesses for pml_axis, pml_layer in enumerate(self.pml_layers): @@ -391,11 +599,59 @@ def plot_pml( pml_box = Box(center=pml_center, size=pml_size) if pml_box.intersects_plane(x=x, y=y, z=z): ax = pml_box.plot(ax=ax, x=x, y=y, z=z, **kwargs) - ax = self.set_plot_bounds(ax=ax, x=x, y=y, z=z) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax - def set_plot_bounds(self, ax: Ax, x: float = None, y: float = None, z: float = None) -> Ax: - """sets the xy limits of the simulation, useful after plotting""" + @add_ax_if_none + def plot_cells(self, x: float = None, y: float = None, z: float = None, ax: Ax = None) -> Ax: + """Plot the cell boundaries as lines on a plane defined by one nonzero x,y,z coordinate. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + cell_boundaries = self.grid.boundaries + axis, _ = self._parse_xyz_kwargs(x=x, y=y, z=z) + _, (axis_x, axis_y) = self.pop_axis([0, 1, 2], axis=axis) + boundaries_x = cell_boundaries.dict()["xyz"[axis_x]] + boundaries_y = cell_boundaries.dict()["xyz"[axis_y]] + for x_pos in boundaries_x: + ax.axvline(x=x_pos, linestyle="-", color="black", linewidth=0.2) + for y_pos in boundaries_y: + ax.axhline(y=y_pos, linestyle="-", color="black", linewidth=0.2) + return ax + + def _set_plot_bounds(self, ax: Ax, x: float = None, y: float = None, z: float = None) -> Ax: + """Sets the xy limits of the simulation at a plane, useful after plotting. + + Parameters + ---------- + ax : matplotlib.axes._subplots.Axes + Matplotlib axes to set bounds on. + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The axes after setting the boundaries. + """ axis, _ = self._parse_xyz_kwargs(x=x, y=y, z=z) _, ((xmin, ymin), (xmax, ymax)) = self._pop_bounds(axis=axis) @@ -408,8 +664,22 @@ def set_plot_bounds(self, ax: Ax, x: float = None, y: float = None, z: float = N def _filter_plot_structures( self, x: float = None, y: float = None, z: float = None ) -> List[Tuple[Medium, Shapely]]: - """Compute list of (medium, shapely) to plot on plane specified by {x,y,z}. + """Compute list of shapes to plot on plane specified by {x,y,z}. Overlaps are removed or merged depending on medium. + + Parameters + ---------- + x : float = None + position of plane in x direction, only one of x, y, z must be specified to define plane. + y : float = None + position of plane in y direction, only one of x, y, z must be specified to define plane. + z : float = None + position of plane in z direction, only one of x, y, z must be specified to define plane. + + Returns + ------- + List[Tuple[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`, shapely.geometry.base.BaseGeometry]] + List of shapes and mediums on the plane after merging. """ shapes = [] @@ -431,8 +701,19 @@ def _filter_plot_structures( @staticmethod def _merge_shapes(shapes: List[Tuple[Medium, Shapely]]) -> List[Tuple[Medium, Shapely]]: - """Merge list of (Medium, Shapely) by intersection with same medium - edit background shapes to remove volume by intersection. + """Merge list of shapes and mediums on plae by intersection with same medium. + Edit background shapes to remove volume by intersection. + + Parameters + ---------- + shapes : List[Tuple[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`, shapely.geometry.base.BaseGeometry]] + Ordered list of shapes and their mediums on a plane. + + Returns + ------- + List[Tuple[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`, shapely.geometry.base.BaseGeometry]] + Shapes and their mediums on a plane + after merging and removing intersections with background. """ background_shapes = [] for medium, shape in shapes: @@ -464,7 +745,14 @@ def _merge_shapes(shapes: List[Tuple[Medium, Shapely]]) -> List[Tuple[Medium, Sh @property def frequency_range(self) -> FreqBound: - """range of frequencies spanning all sources' frequency dependence""" + """Range of frequencies spanning all sources' frequency dependence. + + Returns + ------- + Tuple[float, float] + Minumum and maximum frequencies of the power spectrum of the sources + at 5 standard deviations. + """ freq_min = min(source.frequency_range[0] for source in self.sources) freq_max = max(source.frequency_range[1] for source in self.sources) return (freq_min, freq_max) @@ -473,21 +761,39 @@ def frequency_range(self) -> FreqBound: @property def dt(self) -> float: - """compute time step (distance)""" - dl_mins = [np.min(sizes) for sizes in self.grid.cell_sizes.dict().values()] + """Simulation time step (distance). + + Returns + ------- + float + Time step (seconds). + """ + dl_mins = [np.min(sizes) for sizes in self.grid.sizes.dict().values()] dl_sum_inv_sq = sum([1 / dl ** 2 for dl in dl_mins]) dl_avg = 1 / np.sqrt(dl_sum_inv_sq) return self.courant * dl_avg / C_0 @property def tmesh(self) -> Coords1D: - """compute time steps""" + """FDTD time stepping points. + + Returns + ------- + np.ndarray + Times (seconds) that the simulation time steps through. + """ dt = self.dt return np.arange(0.0, self.run_time + dt, dt) @property def grid(self) -> Grid: - """:class:`Grid` interface to the spatial locations in Simulation""" + """FDTD grid spatial locations and information. + + Returns + ------- + :class:`Grid` + :class:`Grid` storing the spatial locations relevant to the simulation. + """ cell_boundary_dict = {} zipped_vals = zip("xyz", self.grid_size, self.center, self.size, self.num_pml_layers) for key, dl, center, size, num_layers in zipped_vals: @@ -498,14 +804,28 @@ def grid(self) -> Grid: if size_snapped != size: log.warning(f"dl = {dl} not commensurate with simulation size = {size}") bound_coords = center + np.linspace(-size_snapped / 2, size_snapped / 2, num_cells + 1) - bound_coords = self.add_pml_to_bounds(num_layers, bound_coords) + bound_coords = self._add_pml_to_bounds(num_layers, bound_coords) cell_boundary_dict[key] = bound_coords boundaries = Coords(**cell_boundary_dict) return Grid(boundaries=boundaries) @staticmethod - def add_pml_to_bounds(num_layers: Tuple[int, int], bounds: Numpy): - """Append PML pixels at the beginning and end of bounds.""" + def _add_pml_to_bounds(num_layers: Tuple[int, int], bounds: Coords1D): + """Append absorber layers to the beginning and end of the simulation bounds + along one dimension. + + Parameters + ---------- + num_layers : Tuple[int, int] + number of layers in the absorber + and - direction along one dimension. + bound_coords : np.ndarray + coordinates specifying boundaries between cells along one dimension. + + Returns + ------- + np.ndarray + New bound coordinates along dimension taking abosrber into account. + """ if bounds.size < 2: return bounds @@ -519,7 +839,13 @@ def add_pml_to_bounds(num_layers: Tuple[int, int], bounds: Numpy): @property def wvl_mat_min(self) -> float: - """minimum wavelength in the material""" + """Minimum wavelength in the material. + + Returns + ------- + float + Minimum wavelength in the material (microns). + """ freq_max = max(source.source_time.freq0 for source in self.sources) wvl_min = C_0 / min(freq_max) eps_max = max(abs(structure.medium.get_eps(freq_max)) for structure in self.structures) @@ -527,7 +853,18 @@ def wvl_mat_min(self) -> float: return wvl_min / n_max def discretize(self, box: Box) -> Grid: - """returns subgrid containing only cells that intersect with Box""" + """Grid containing only cells that intersect with a :class:`Box`. + + Parameters + ---------- + box : :class:`Box` + Rectangular geometry within simulation to discretize. + + Returns + ------- + :class:`Grid` + The FDTD subgrid containing simulation points that intersect with ``box``. + """ if not self.intersects(box): log.error(f"Box {box} is outside simulation, cannot discretize") @@ -558,7 +895,27 @@ def discretize(self, box: Box) -> Grid: return Grid(boundaries=sub_boundaries) def epsilon(self, box: Box, freq: float = None) -> Dict[str, xr.DataArray]: - """get data of permittivity at volume specified by box and freq""" + """Get array of permittivity at volume specified by box and freq + + Parameters + ---------- + box : :class:`Box` + Rectangular geometry specifying where to measure the permittivity. + freq : float = None + The frequency to evaluate the mediums at. + If not specified, evaluates at infinite frequency. + + Returns + ------- + Dict[str, xarray.DataArray] + Mapping of coordinate type to xarray DataArray containing permittivity data. + keys of dict are ``{'centers', 'boundaries', 'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'}``. + ``'centers'`` contains the permittivity at the yee cell centers. + `'boundaries'`` contains the permittivity at the corner intersections between yee cells. + ``'Ex'`` and other field keys contain the permittivity + at the corresponding field position in the yee lattice. + For details on xarray datasets, refer to `xarray's Documentaton `_. + """ sub_grid = self.discretize(box) eps_background = self.medium.eps_model(freq) diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index 50ac03933e..ff8ab27e98 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -1,44 +1,62 @@ -"""Defines current sources.""" +"""Defines electric current sources for injecting light into simulation.""" from abc import ABC, abstractmethod -from typing import Tuple, Union, Literal +from typing import Union, Literal import pydantic import numpy as np from .base import Tidy3dBaseModel -from .types import Direction, Polarization, Ax, FreqBound +from .types import Direction, Polarization, Ax, FreqBound, Array from .validators import assert_plane, validate_name_str from .geometry import Box from .mode import Mode from .viz import add_ax_if_none, SourceParams from ..log import ValidationError +from ..constants import inf # pylint:disable=unused-import + +# TODO: change directional source to something signifying its intent is to create a specific field. + +# width of pulse frequency range defition in units of standard deviation. +WIDTH_STD = 5 class SourceTime(ABC, Tidy3dBaseModel): - """Base class describing the time dependence of a source""" + """Base class describing the time dependence of a source.""" amplitude: pydantic.NonNegativeFloat = 1.0 phase: float = 0.0 @abstractmethod - def amp_time(self, time): + def amp_time(self, time: float) -> complex: """Complex-valued source amplitude as a function of time. - Args: - time (float): time in seconds. + Parameters + ---------- + time : float + Time in seconds. + + Returns + ------- + complex + Complex-valued source amplitude at that time.. """ @add_ax_if_none - def plot(self, times: float, ax: Ax = None) -> Ax: - """plot the time series - - Args: - times (float): Description - ax (Ax, optional): Description - - Returns: - Ax: Description + def plot(self, times: Array[float], ax: Ax = None) -> Ax: + """Plot the complex-valued amplitude of the source time-dependence. + + Parameters + ---------- + times : np.ndarray + Array of times to plot source at in seconds. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. """ times = np.array(times) amp_complex = self.amp_time(times) @@ -56,11 +74,11 @@ def plot(self, times: float, ax: Ax = None) -> Ax: @property @abstractmethod def frequency_range(self) -> FreqBound: - """frequency range for a source time""" + """Frequency range within 5 standard deviations of the central frequency.""" class Pulse(SourceTime, ABC): - """Source ramps up and oscillates with freq0""" + """A source time that ramps up with some ``fwidth`` and oscillates at ``freq0``.""" freq0: pydantic.PositiveFloat fwidth: pydantic.PositiveFloat # currently standard deviation @@ -68,23 +86,53 @@ class Pulse(SourceTime, ABC): @property def frequency_range(self) -> FreqBound: - """frequency range for a source time""" - width_std = 5 - return (self.freq0 - width_std * self.fwidth, self.freq0 + width_std * self.fwidth) + """Frequency range within 5 standard deviations of the central frequency. + + Returns + ------- + Tuple[float, float] + Minimum and maximum frequencies of the + :class:`GaussianPulse` or :class:`ContinuousWave` power + within 5 standard deviations. + """ + return (self.freq0 - WIDTH_STD * self.fwidth, self.freq0 + WIDTH_STD * self.fwidth) class GaussianPulse(Pulse): - """A gaussian pulse time dependence""" - - def amp_time(self, time): - """complex amplitude as a function of time + """Source time dependence that describes a Gaussian pulse. + + Parameters + ---------- + freq0 : float + Central oscillating frequency in Hz. + Must be positive. + fwidth : float + Standard deviation width of the Gaussian pulse in Hz. + Must be positive. + offset : float = 5.0 + Time of the maximum value of the pulse + in units of 1/fwidth. + Must be greater than 2.5. + + Example + ------- + >>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12) + """ + + def amp_time(self, time: float) -> complex: + """Complex-valued source amplitude as a function of time. - Args: - time (TYPE): Description + Parameters + ---------- + time : float + Time in seconds. - Returns: - TYPE: Description + Returns + ------- + complex + Complex-valued source amplitude at supplied time. """ + twidth = 1.0 / (2 * np.pi * self.fwidth) omega0 = 2 * np.pi * self.freq0 time_shifted = time - self.offset * twidth @@ -97,17 +145,40 @@ def amp_time(self, time): return const * offset * oscillation * amp -class CW(Pulse): - """ramping up and holding steady""" - - def amp_time(self, time): - """complex amplitude as a function of time +class ContinuousWave(Pulse): + """Source time dependence that ramps up to continuous oscillation + and holds until end of simulation. + + Parameters + ---------- + freq0 : float + Central oscillating frequency in Hz. + Must be positive. + fwidth : float + Standard deviation width of the Gaussian pulse in Hz. + Must be positive. + offset : float = 5.0 + Time of the maximum value of the pulse + in units of 1/fwidth. + Must be greater than 2.5. + + Example + ------- + >>> cw = ContinuousWave(freq0=200e12, fwidth=20e12) + """ + + def amp_time(self, time: float) -> complex: + """Complex-valued source amplitude as a function of time. - Args: - time (TYPE): Description + Parameters + ---------- + time : float + Time in seconds. - Returns: - TYPE: Description + Returns + ------- + complex + Complex-valued source amplitude at supplied time. """ twidth = 1.0 / (2 * np.pi * self.fwidth) omega0 = 2 * np.pi * self.freq0 @@ -121,13 +192,26 @@ def amp_time(self, time): return const * offset * oscillation * amp -SourceTimeType = Union[GaussianPulse, CW] +SourceTimeType = Union[GaussianPulse, ContinuousWave] """ Source objects """ class Source(Box, ABC): - """Template for all sources, all have Box geometry""" + """Abstract base class for all sources. + + Parameters + ---------- + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of source in x,y,z. + size : Tuple[float, float, float] + Size of source in x,y,z. + All elements must be non-negative. + source_time : :class:`GaussianPulse` or :class:`ContinuousWave` + Specification of time-dependence of source. + name : str = None + Optional name for source. + """ source_time: SourceTimeType name: str = None @@ -138,26 +222,101 @@ class Source(Box, ABC): def plot( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: - """plot source geometry""" + """Plot the source geometry on a cross section plane. + + Parameters + ---------- + x : float = None + Position of plane in x direction, only one of x,y,z can be specified to define plane. + y : float = None + Position of plane in y direction, only one of x,y,z can be specified to define plane. + z : float = None + Position of plane in z direction, only one of x,y,z can be specified to define plane. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + **patch_kwargs + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. #pylint:disable=line-too-long + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ kwargs = SourceParams().update_params(**kwargs) ax = self.geometry.plot(x=x, y=y, z=z, ax=ax, **kwargs) return ax @property def geometry(self): - """box representation of self""" + """:class:`Box` representation of source. + + Returns + ------- + :class:`Box` + Representation of the source geometry as a :class:`Box`. + """ return Box(center=self.center, size=self.size) class VolumeSource(Source): - """Volume Source with time dependence and polarization""" + """Source spanning a rectangular volume with uniform time dependence. + + Parameters + ---------- + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of source in x,y,z. + size : Tuple[float, float, float] + Size of source in x,y,z. + All elements must be non-negative. + source_time : :class:`GaussianPulse` or :class:`ContinuousWave` + Specification of time-dependence of source. + polarization : str + Specifies the direction and type of current component. + Must be in ``{'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'}``. + For example, ``'Ez'`` specifies electric current source polarized along the z-axis. + name : str = None + Optional name for source. + + Example + ------- + >>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12) + >>> pt_source = VolumeSource(size=(0,0,0), source_time=pulse, polarization='Ex') + """ polarization: Polarization type: Literal["VolumeSource"] = "VolumeSource" class ModeSource(Source): - """Modal profile on finite extent plane""" + """Modal profile on finite extent plane + + Parameters + ---------- + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of source in x,y,z. + size : Tuple[float, float, float] + Size of source in x,y,z. + One component must be 0.0 to define plane. + All elements must be non-negative. + source_time : :class:`GaussianPulse` or :class:`ContinuousWave` + Specification of time-dependence of source. + direction : str + Specifies the sign of propagation. + Must be in ``{'+', '-'}``. + Note: propagation occurs along dimension normal to plane. + mode : :class:`Mode` + Specification of the mode being injected by source. + name : str = None + Optional name for source. + + Example + ------- + >>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12) + >>> mode = Mode(mode_index=1, num_modes=3) + >>> mode_source = ModeSource(size=(10,10,0), source_time=pulse, mode=mode, direction='-') + """ direction: Direction mode: Mode @@ -166,7 +325,7 @@ class ModeSource(Source): class DirectionalSource(Source, ABC): - """A Planar Source with uni-directional propagation""" + """A Planar Source with uni-directional propagation.""" direction: Direction polarization: Polarization @@ -175,7 +334,7 @@ class DirectionalSource(Source, ABC): @pydantic.root_validator(allow_reuse=True) def polarization_is_orthogonal(cls, values): - """ensure we dont allow a polarization parallel to the propagation direction""" + """Ensure the polarization is orthogonal to the propagation direction.""" size = values.get("size") polarization = values.get("polarization") assert size is not None @@ -193,17 +352,90 @@ def polarization_is_orthogonal(cls, values): class PlaneWave(DirectionalSource): - """uniform distribution on infinite extent plane""" + """Uniform distribution on infinite extent plane. + + Parameters + ---------- + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of source in x,y,z. + size : Tuple[float, float, float] + Size of source in x,y,z. + One component must be 0.0 to define plane. + All elements must be non-negative. + source_time : :class:`GaussianPulse` or :class:`ContinuousWave` + Specification of time-dependence of source. + polarization : str + Specifies the direction and type of current component. + Must be in ``{'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'}``. + For example, ``'Ez'`` specifies electric current source polarized along the z-axis. + direction : str + Specifies the sign of propagation. + Must be in ``{'+', '-'}``. + Note: propagation occurs along dimension normal to plane. + mode : :class:`Mode` + Specification of the mode being injected by source. + name : str = None + Optional name for source. + + Example + ------- + >>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12) + >>> pw_source = PlaneWave(size=(inf,0,inf), source_time=pulse, polarization='Ex', direction='+') + """ type: Literal["PlaneWave"] = "PlaneWave" class GaussianBeam(DirectionalSource): - """guassian distribution on finite extent plane""" - - waist_size: Tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat] + """guassian distribution on finite extent plane + + Parameters + ---------- + center : Tuple[float, float, float] = (0.0, 0.0, 0.0) + Center of source in x,y,z. + size : Tuple[float, float, float] + Size of source in x,y,z. + One component must be 0.0 to define plane. + All elements must be non-negative. + source_time : :class:`GaussianPulse` or :class:`ContinuousWave` + Specification of time-dependence of source. + polarization : str + Specifies the direction and type of current component. + Must be in ``{'Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'}``. + For example, ``'Ez'`` specifies electric current source polarized along the z-axis. + direction : str + Specifies the sign of propagation. + Must be in ``{'+', '-'}``. + Note: propagation occurs along dimension normal to plane. + waist_radius: float + Radius of the beam at the waist (um). + Must be positive. + angle_theta: float = 0.0 + Angle of propagation of the beam with respect to the normal axis (rad). + angle_phi: float = 0.0 + Angle of propagation of the beam with respect to parallel axis (rad). + pol_angle: float = 0.0 + Angle of the polarization with respect to the parallel axis (rad). + name : str = None + Optional name for source. + + Example + ------- + >>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12) + >>> gauss = GaussianBeam( + ... size=(0,3,3), + ... source_time=pulse, + ... polarization='Hy', + ... direction='+', + ... waist_radius=1.0) + """ + + waist_radius: pydantic.PositiveFloat + angle_theta: float = 0.0 + angle_phi: float = 0.0 + pol_angle: float = 0.0 type: Literal["GaussianBeam"] = "GaussianBeam" -# allowable sources to use in Simulation.sources +# sources allowed in Simulation.sources SourceType = Union[VolumeSource, PlaneWave, ModeSource, GaussianBeam] diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index 515b1c5ba5..1a7642dfa2 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -1,15 +1,33 @@ -""" defines Geometric objects with Medium properties """ +"""Defines Geometric objects with Medium properties.""" from .base import Tidy3dBaseModel from .validators import validate_name_str -from .geometry import GeometryType -from .medium import MediumType +from .geometry import GeometryType, Box # pylint: disable=unused-import +from .medium import MediumType, Medium # pylint: disable=unused-import from .types import Ax from .viz import add_ax_if_none class Structure(Tidy3dBaseModel): - """An object that interacts with the electromagnetic fields""" + """Defines a physical object that interacts with the electromagnetic fields. + A :class:`Structure` is a combination of a material property (:class:`AbstractMedium`) + and a :class:`Geometry`. + + Parameters + ---------- + geometry : :class:`Geometry` + Defines spatial extent of the :class:`Structure`. + medium : :class:`AbstractMedium` + Defines the electromagnetic properties of the structure material. + name : str = None + Optional name for the structure, used for plotting and logging. + + Example + ------- + >>> box = Box(center=(0,0,1), size=(2, 2, 2)) + >>> glass = Medium(permittivity=3.9) + >>> struct = Structure(geometry=box, medium=glass, name='glass_box') + """ geometry: GeometryType medium: MediumType @@ -21,24 +39,27 @@ class Structure(Tidy3dBaseModel): def plot( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **patch_kwargs ) -> Ax: - """Plot structure geometry cross section at single (x,y,z) coordinate. + """Plot structure geometry cross section. + Note: only one of x, y, or z must be specified to define cross section. Parameters ---------- - x : ``float``, optional + x : float = None Position of plane in x direction. - y : ``float``, optional + y : float = None Position of plane in y direction. - z : ``float``, optional + z : float = None Position of plane in z direction. - ax : ``matplotlib.axes._subplots.Axes``, optional + ax : matplotlib.axes._subplots.Axes = None matplotlib axes to plot on, if not specified, one is created. **patch_kwargs - Optional keyword arguments passed to ``add_artist(patch, **patch_kwargs)``. + Optional keyword arguments passed to the matplotlib patch plotting of structure. + For details on accepted values, refer to + `Matplotlib's documentation `_. #pylint:disable=line-too-long Returns ------- - ``matplotlib.axes._subplots.Axes`` + matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ return self.geometry.plot(x=x, y=y, z=z, ax=ax, **patch_kwargs) diff --git a/tidy3d/components/types.py b/tidy3d/components/types.py index 20f2ad84d6..65a1edf7bd 100644 --- a/tidy3d/components/types.py +++ b/tidy3d/components/types.py @@ -31,13 +31,17 @@ Vertices = List[Coordinate2D] Shapely = BaseGeometry -""" grid """ +""" medium """ -""" medium """ +class ComplexNumber(pydantic.BaseModel): + """Holds real and imaginary parts of a complex number.""" + + real: float + imag: float -Complex = Tuple[float, float] -PoleAndResidue = Tuple[Complex, Complex] + +PoleAndResidue = Tuple[complex, complex] FreqBound = Union[float, Inf, Literal[-inf]] """ symmetries """ @@ -91,14 +95,20 @@ class Array(np.ndarray, metaclass=ArrayMeta): """type of numpy array with annotated type (Array[float], Array[complex])""" +""" note: + ^ this is the best way to declare numpy types if you know dtype. + for example: ``field_amps: Array[float] = np.random.random(5)``. +""" + # lists or np.ndarrays of certain type FloatArrayLike = Union[List[float], Array[float]] IntArrayLike = Union[List[int], Array[int]] ComplexArrayLike = Union[List[complex], Array[complex]] -# encoding for JSON in pydantic models +# encoding for JSON in tidy3d data +# technically not used yet since the tidy3d data has separate load and export methods. def numpy_encoding(array): - """json encoding of numpy array""" + """json encoding of a (maybe complex-valued) numpy array.""" if array.dtype == "complex": return {"re": list(array.real), "im": list(array.imag)} return list(array) diff --git a/tidy3d/components/validators.py b/tidy3d/components/validators.py index 6bcc2b4c21..25d001c896 100644 --- a/tidy3d/components/validators.py +++ b/tidy3d/components/validators.py @@ -7,6 +7,39 @@ from .geometry import Box +""" Explanation of pydantic validators: + + Validators are class methods that are added to the models to validate their fields (kwargs). + The functions on this page return validators based on config arguments + and are generally in multiple components of tidy3d. + The inner functions (validators) are decorated with @pydantic.validator, which is configured. + First argument is the string of the field being validated in the model. + ``allow_reuse`` lets us use the validator in more than one model. + ``always`` makes sure if the model is changed, the validator gets called again. + + The function being decorated by @pydantic.validator generally takes + ``cls`` the class that the validator is added to. + ``val`` the value of the field being validated. + ``values`` a dictionary containing all of the other fields of the model. + It is important to note that the validator only has access to fields that are defined + before the field being validated. + Fields defined under the validated field will not be in ``values``. + + All validators generally should throw an exception if the validation fails + and return val if it passes. + Sometimes, we can use validators to change ``val`` or ``values``, + but this should be done with caution as it can be hard to reason about. + + To add a validator from this file to the pydantic model, + put it in the model's main body and assign it to a variable (class method). + For example ``_plane_validator = assert_plane()``. + Note, if the assigned name ``_plane_validator`` is used later on for another validator, say, + the original validator will be overwritten so be aware of this. + + For more details: `Pydantic Validators `_ +""" + + def assert_plane(): """makes sure a field's `size` attribute has exactly 1 zero""" diff --git a/tidy3d/components/viz.py b/tidy3d/components/viz.py index 2898ccd833..4a5f3a6548 100644 --- a/tidy3d/components/viz.py +++ b/tidy3d/components/viz.py @@ -9,21 +9,23 @@ from pydantic import BaseModel from .types import Ax +from ..constants import pec_val def make_ax() -> Ax: - """makes an empty `ax`""" + """makes an empty `ax`.""" _, ax = plt.subplots(1, 1, tight_layout=True) return ax def add_ax_if_none(plot): - """decorates `plot(ax=None)` function, - if ax=None, creates ax and feeds it to `plot`. + """Decorates `plot(*args, **kwargs, ax=None)` function. + if ax=None in the function call, creates an ax and feeds it to rest of function. """ @wraps(plot) def _plot(*args, **kwargs) -> Ax: + """New plot function using a generated ax if None.""" if kwargs.get("ax") is None: ax = make_ax() kwargs["ax"] = ax @@ -32,101 +34,111 @@ def _plot(*args, **kwargs) -> Ax: return _plot +""" Utilities for default plotting parameters.""" + + class PatchParams(BaseModel): - """holds parameters for matplotlib.patches - https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html + """Datastructure holding default parameters for plotting matplotlib.patches. + See https://matplotlib.org/stable/api/_as_gen/matplotlib.patches.Patch.html for explanation. """ alpha: Any = None edgecolor: Any = None facecolor: Any = None fill: bool = True + hatch: str = None class PatchParamSwitcher(BaseModel): - """base class for updating parameters based on default values""" + """Class used for updating plot kwargs based on :class:`PatchParams` values.""" def update_params(self, **plot_params): - """return dictionary of plot params updated with fields user supplied **plot_params dict""" + """return dictionary of plot params updated with fields user supplied **plot_params dict.""" + + # update self's plot params with the the supplied plot parameters and return non none ones. default_plot_params = self.get_plot_params() default_plot_params_dict = default_plot_params.dict().copy() default_plot_params_dict.update(plot_params) - # get rid of pairs with value of None as they will mess up plots down the line + # get rid of pairs with value of None as they will mess up plots down the line. return {key: val for key, val in default_plot_params_dict.items() if val is not None} @abstractmethod def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args. Implement in subclasses.""" class GeoParams(PatchParamSwitcher): - """Patch plotting parameters for td.Geometry""" + """Patch plotting parameters for :class:`Geometry`.""" def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" - return PatchParams(edgecolor="black", facecolor="cornflowerblue") + """Returns :class:`PatchParams` based on user-supplied args.""" + return PatchParams(edgecolor=None, facecolor="cornflowerblue") class SourceParams(PatchParamSwitcher): - """Patch plotting parameters for `td.Source`""" + """Patch plotting parameters for :class:`Source`.""" def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args.""" return PatchParams(alpha=0.7, facecolor="blueviolet", edgecolor="blueviolet") class MonitorParams(PatchParamSwitcher): - """Patch plotting parameters for `td.Monitor`""" + """Patch plotting parameters for :class:`Monitor`.""" def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args.""" return PatchParams(alpha=0.7, facecolor="crimson", edgecolor="crimson") class StructMediumParams(PatchParamSwitcher): - """Patch plotting parameters for `td.Structures in `td.Simulation.plot_structures`""" + """Patch plotting parameters for :class:`Structures` in ``Simulation.plot_structures``.""" medium: Any medium_map: dict def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args.""" mat_index = self.medium_map[self.medium] mat_cmap = cm.Set2 # pylint: disable=no-name-in-module, no-member facecolor = mat_cmap(mat_index % len(mat_cmap.colors)) - return PatchParams(facecolor=facecolor) + if self.medium.name == "PEC": + return PatchParams(facecolor="black", edgecolor="black", lw=0) + return PatchParams(facecolor=facecolor, edgecolor=facecolor, lw=0) class StructEpsParams(PatchParamSwitcher): - """Patch plotting parameters for `td.Structures in `td.Simulation.plot_structures_eps`""" + """Patch plotting parameters for :class:`Structures` in `td.Simulation.plot_structures_eps`.""" eps: float eps_max: float def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args.""" chi = self.eps - 1.0 chi_max = self.eps_max - 1.0 color = 1 - chi / chi_max - return PatchParams(facecolor=str(color)) + if self.eps == pec_val: + return PatchParams(facecolor="gold", edgecolor="k", lw=1) + return PatchParams(facecolor=str(color), edgecolor=str(color), lw=0) class PMLParams(PatchParamSwitcher): - """Patch plotting parameters for `td.Simulation.pml_layers`""" + """Patch plotting parameters for :class:`AbsorberSpec` (PML).""" def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args.""" return PatchParams(alpha=0.7, facecolor="sandybrown", edgecolor="sandybrown") class SymParams(PatchParamSwitcher): - """Patch plotting parameters for `td.Simulation.symmetry`""" + """Patch plotting parameters for `td.Simulation.symmetry`.""" sym_value: int def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args.""" if self.sym_value == 1: return PatchParams(alpha=0.5, facecolor="lightsteelblue", edgecolor="lightsteelblue") if self.sym_value == -1: @@ -135,8 +147,8 @@ def get_plot_params(self) -> PatchParams: class SimDataGeoParams(PatchParamSwitcher): - """Patch plotting parameters for `td.Simulation.symmetry`""" + """Patch plotting parameters for `td.SimulationData`.""" def get_plot_params(self) -> PatchParams: - """returns PatchParams based on attributes of self""" + """Returns :class:`PatchParams` based on user-supplied args.""" return PatchParams(alpha=0.4, edgecolor="black") diff --git a/tidy3d/constants.py b/tidy3d/constants.py index 6a7b99194d..6c9145c590 100644 --- a/tidy3d/constants.py +++ b/tidy3d/constants.py @@ -1,18 +1,32 @@ # pylint: disable=invalid-name -""" defines constants used elsewhere in the package""" +"""Defines importable constants. + +Attributes: + inf (float): Tidy3d representation of infinity. + C_0 (float): Speed of light in vacuum [um/s] + EPSILON_0 (float): Vacuum permittivity [F/um] + MU_0 (float): Vacuum permeability [H/um] + ETA_0 (float): Vacuum impedance + HBAR (float): reduced Planck constant [eV*s] + Q_e (float): funamental charge [C] +""" import numpy as np -EPSILON_0 = np.float32(8.85418782e-18) # vacuum permittivity [F/um] -MU_0 = np.float32(1.25663706e-12) # vacuum permeability [H/um] -C_0 = 1 / np.sqrt(EPSILON_0 * MU_0) # speed of light in vacuum [um/s] -ETA_0 = np.sqrt(MU_0 / EPSILON_0) # vacuum impedance -Q_e = 1.602176634e-19 # funamental charge -HBAR = 6.582119569e-16 # reduced Planck constant [eV*s] +# fundamental constants +EPSILON_0 = np.float32(8.85418782e-18) +MU_0 = np.float32(1.25663706e-12) +C_0 = 1 / np.sqrt(EPSILON_0 * MU_0) +ETA_0 = np.sqrt(MU_0 / EPSILON_0) +Q_e = 1.602176634e-19 +HBAR = 6.582119569e-16 -inf = 1e16 +# infinity (very large) +inf = 1e20 +# floating point precisions dp_eps = np.finfo(np.float64).eps fp_eps = np.finfo(np.float32).eps +# values of PEC for mode solver pec_val = -1e8 diff --git a/tidy3d/convert.py b/tidy3d/convert.py index 3a0b9b08a1..708af10e5e 100644 --- a/tidy3d/convert.py +++ b/tidy3d/convert.py @@ -6,7 +6,8 @@ from tidy3d import Simulation, SimulationData, FieldData, data_type_map from tidy3d import Box, Sphere, Cylinder, PolySlab -from tidy3d import Medium # , DispersiveMedium +from tidy3d import Medium, AnisotropicMedium +from tidy3d.components.medium import DispersiveMedium, PECMedium from tidy3d import VolumeSource, ModeSource, PlaneWave from tidy3d import GaussianPulse from tidy3d import PML, Absorber, StablePML @@ -41,7 +42,7 @@ def old_json_parameters(sim: Simulation) -> Dict: profile = "standard" pml_layers.append({"profile": profile, "Nlayers": pml.num_layers}) - sizes = sim.grid.cell_sizes + sizes = sim.grid.sizes mesh_step_x = np.mean(sizes.x) mesh_step_y = np.mean(sizes.y) mesh_step_z = np.mean(sizes.z) @@ -96,29 +97,42 @@ def old_json_structures(sim: Simulation) -> Tuple[List[Dict], List[Dict]]: } ) - """ TODO: Dispersive mediums need to eventually be defined as pole residue pairs for the - solver. Seems like this will have to be done on the server side in the revamp. """ - # elif isinstance(medium, DispersiveMedium): - # poles = [] - # for pole in medium.poles: - # poles.append([pole[0].real, pole[0].imag, pole[1].real, pole[1].imag]) - # med.update( - # { - # "type": "Medium", - # "permittivity": [medium.eps_inf] * 3, - # "conductivity": [0, 0, 0], - # "poles": poles, - # } - # ) - - """ TODO: support PEC. Note that PMC is probably not needed (not supported currently).""" - # elif isinstance(medium, PEC): - # med.update({"type": "PEC"}) + elif isinstance(medium, AnisotropicMedium): + """TODO: support diagonal anisotropy in non-dispersive media""" + med.update( + { + "type": "Medium", + "permittivity": [ + medium.xx.permittivity, + medium.yy.permittivity, + medium.zz.permittivity, + ], + "conductivity": [ + medium.xx.conductivity, + medium.yy.conductivity, + medium.zz.conductivity, + ], + "poles": [], + } + ) + elif isinstance(medium, DispersiveMedium): + poles = [] + for (a, c) in medium.pole_residue.poles: + poles.append([a[0], a[1], c[0], c[1]]) + med.update( + { + "type": "PoleResidue", + "permittivity": [medium.pole_residue.eps_inf] * 3, + "conductivity": [0, 0, 0], + "poles": poles, + } + ) + elif isinstance(medium, PECMedium): + med.update({"type": "PEC"}) medium_list.append(med) struct_list = [] for structure in sim.structures: - """TODO: Shouldn't structures also have custom names?""" struct = {"name": structure.medium.name, "mat_index": medium_map[structure.medium]} geom = structure.geometry if isinstance(geom, Box): diff --git a/tidy3d/log.py b/tidy3d/log.py index 0bf7df6c8e..e827946b43 100644 --- a/tidy3d/log.py +++ b/tidy3d/log.py @@ -1,42 +1,74 @@ -""" logging for tidy3d""" +"""Logging and error-handling for Tidy3d.""" import logging from rich.logging import RichHandler +# TODO: more logging features (to file, etc). + FORMAT = "%(message)s" logging.basicConfig(level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]) log = logging.getLogger("rich") +level_map = { + "error": 40, + "warning": 30, + "info": 20, + "debug": 10, +} + class Tidy3DError(Exception): - """any error in tidy3d""" + """Any error in tidy3d""" def __init__(self, message: str = None): - """log the error message and then raise the Exception""" + """Log just the error message and then raise the Exception.""" log.error(message) super().__init__(self, message) +class ConfigError(Tidy3DError): + """Error when configuring Tidy3d.""" + + +class Tidy3dKeyError(Tidy3DError): + """Could not find a key in a Tidy3d dictionary.""" + + class ValidationError(Tidy3DError): - """error when constructing tidy3d components""" + """eError when constructing Tidy3d components.""" class SetupError(Tidy3DError): - """error regarding the setup of the components (outside of domains, etc)""" + """Error regarding the setup of the components (outside of domains, etc).""" class FileError(Tidy3DError): - """error reading or writing to file""" + """Error reading or writing to file.""" class WebError(Tidy3DError): - """error with the webAPI""" + """Error with the webAPI.""" class AuthenticationError(Tidy3DError): - """error authenticating a user through webapi webAPI""" + """Error authenticating a user through webapi webAPI.""" class DataError(Tidy3DError): - """error accessing data""" + """Error accessing data.""" + + +def logging_level(level: str = "info") -> None: + """Set tidy3d logging level priority. + + Parameters + ---------- + level : str + One of ``{'debug', 'info', 'warning', 'error'}`` (listed in increasing priority). + the lowest priority level of logging messages to display. + """ + if level not in level_map: + raise ConfigError(f"logging level {level} not supported, must be in {level_map.keys()}.") + level_int = level_map[level] + log.setLevel(level_int) diff --git a/tidy3d/material_library.py b/tidy3d/material_library.py index 117e614e4f..b7759fb3a6 100644 --- a/tidy3d/material_library.py +++ b/tidy3d/material_library.py @@ -20,9 +20,9 @@ References ---------- - * A. D. Rakic et al., Applied Optics, 37, 5271-5283 (1998) + * A. D. Rakic et al., Applied Optics + 1j*37 + 1j*5271-5283 (1998) * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, - Phys. Rev. B 6, 4370-4379 (1972). + Phys. Rev. B 6 + 1j*4370-4379 (1972). Aluminum ("Al") @@ -43,7 +43,7 @@ * A. D. Rakic. Algorithm for the determination of intrinsic optical constants of metal films: application to aluminum, - Appl. Opt. 34, 4755-4767 (1995) + Appl. Opt. 34 + 1j*4755-4767 (1995) Alumina ("Al2O3") @@ -84,7 +84,7 @@ References ---------- - * R.E. Fern and A. Onton, J. Applied Physics, 42, 3499-500 (1971) + * R.E. Fern and A. Onton, J. Applied Physics + 1j*42 + 1j*3499-500 (1971) * Horiba Technical Note 08: Lorentz Dispersion Model `[pdf] `_. @@ -185,7 +185,7 @@ References ---------- - * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6, 4370-4379 (1972) + * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6 + 1j*4370-4379 (1972) N-BK7 borosilicate glass ("BK7") @@ -220,7 +220,7 @@ * A. D. Rakic. Algorithm for the determination of intrinsic optical constants of metal films: application to aluminum, - Appl. Opt. 34, 4755-4767 (1995) + Appl. Opt. 34 + 1j*4755-4767 (1995) Calcium fluoride ("CaF2") @@ -261,7 +261,7 @@ * N. Sultanova, S. Kasarova and I. Nikolov. Dispersion properties of optical polymers, - Acta Physica Polonica A 116, 585-587 (2009) + Acta Physica Polonica A 116 + 1j*585-587 (2009) Chromium ("Cr") @@ -282,7 +282,7 @@ * A. D. Rakic. Algorithm for the determination of intrinsic optical constants of metal films: application to aluminum, - Appl. Opt. 34, 4755-4767 (1995) + Appl. Opt. 34 + 1j*4755-4767 (1995) Copper ("Cu") @@ -301,7 +301,7 @@ References ---------- - * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6, 4370-4379 (1972) + * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6 + 1j*4370-4379 (1972) Fused silica ("FusedSilica") @@ -321,10 +321,10 @@ ---------- * I. H. Malitson. Interspecimen comparison of the refractive index of - fused silica, J. Opt. Soc. Am. 55, 1205-1208 (1965) + fused silica, J. Opt. Soc. Am. 55 + 1j*1205-1208 (1965) * C. Z. Tan. Determination of refractive index of silica glass for infrared wavelengths by IR spectroscopy, - J. Non-Cryst. Solids 223, 158-163 (1998) + J. Non-Cryst. Solids 223 + 1j*158-163 (1998) Gallium arsenide ("GaAs") @@ -346,7 +346,7 @@ * T. Skauli, P. S. Kuo, K. L. Vodopyanov, T. J. Pinguet, O. Levi, L. A. Eyres, J. S. Harris, M. M. Fejer, B. Gerard, L. Becouarn, and E. Lallier. Improved dispersion relations for GaAs and - applications to nonlinear optics, J. Appl. Phys., 94, 6447-6455 (2003) + applications to nonlinear optics, J. Appl. Phys. + 946447-6455 (2003) Germanium ("Ge") @@ -488,12 +488,12 @@ References ---------- - * Handbook of Optics, 2nd edition, Vol. 2. McGraw-Hill 1994 + * Handbook of Optics + 1j*2nd edition, Vol. 2. McGraw-Hill 1994 * G. D. Pettit and W. J. Turner. Refractive index of InP, - J. Appl. Phys. 36, 2081 (1965) + J. Appl. Phys. 36 + 1j*2081 (1965) * A. N. Pikhtin and A. D. Yaskov. Disperson of the refractive index of semiconductors with diamond and zinc-blende structures, - Sov. Phys. Semicond. 12, 622-626 (1978) + Sov. Phys. Semicond. 12 + 1j*622-626 (1978) Magnesium fluoride ("MgF2") @@ -552,7 +552,7 @@ References ---------- - * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6, 4370-4379 (1972) + * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6 + 1j*4370-4379 (1972) Polyetherimide ("PEI") @@ -634,7 +634,7 @@ `[pdf] `_. * N. Sultanova, S. Kasarova and I. Nikolov. Dispersion properties of optical polymers, - Acta Physica Polonica A 116, 585-587 (2009) + Acta Physica Polonica A 116 + 1j*585-587 (2009) Polytetrafluoroethylene, or Teflon ("PTFE") @@ -693,7 +693,7 @@ References ---------- - * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6, 4370-4379 (1972) + * P. B. Johnson and R. W. Christy. Optical constants of the noble metals, Phys. Rev. B 6 + 1j*4370-4379 (1972) Polycarbonate. ("Polycarbonate") @@ -718,7 +718,7 @@ `[pdf] `_. * N. Sultanova, S. Kasarova and I. Nikolov. Dispersion properties of optical polymers, - Acta Physica Polonica A 116, 585-587 (2009) + Acta Physica Polonica A 116 + 1j*585-587 (2009) Polystyrene. ("Polystyrene") @@ -739,7 +739,7 @@ * N. Sultanova, S. Kasarova and I. Nikolov. Dispersion properties of optical polymers, - Acta Physica Polonica A 116, 585-587 (2009) + Acta Physica Polonica A 116 + 1j*585-587 (2009) Platinum ("Pt") @@ -760,7 +760,7 @@ * W. S. M. Werner, K. Glantschnig, C. Ambrosch-Draxl. Optical constants and inelastic electron-scattering data for 17 - elemental metals, J. Phys Chem Ref. Data 38, 1013-1092 (2009) + elemental metals, J. Phys Chem Ref. Data 38 + 1j*1013-1092 (2009) Sapphire. ("Sapphire") @@ -803,13 +803,13 @@ References ---------- - * T. Baak. Silicon oxynitride; a material for GRIN optics, Appl. Optics 21, 1069-1072 (1982) + * T. Baak. Silicon oxynitride; a material for GRIN optics, Appl. Optics 21 + 1j*1069-1072 (1982) * Horiba Technical Note 08: Lorentz Dispersion Model `[pdf] `_. * K. Luke, Y. Okawachi, M. R. E. Lamont, A. L. Gaeta, M. Lipson. Broadband mid-infrared frequency comb generation in a Si3N4 microresonator, - Opt. Lett. 40, 4823-4826 (2015) - * H. R. Philipp. Optical properties of silicon nitride, J. Electrochim. Soc. 120, 295-300 (1973) + Opt. Lett. 40 + 1j*4823-4826 (2015) + * H. R. Philipp. Optical properties of silicon nitride, J. Electrochim. Soc. 120 + 1j*295-300 (1973) Silicon carbide ("SiC") @@ -930,7 +930,7 @@ * W. S. M. Werner, K. Glantschnig, C. Ambrosch-Draxl. Optical constants and inelastic electron-scattering data for 17 - elemental metals, J. Phys Chem Ref. Data 38, 1013-1092 (2009) + elemental metals, J. Phys Chem Ref. Data 38 + 1j*1013-1092 (2009) Titanium oxide ("TiOx") @@ -971,7 +971,7 @@ * W. S. M. Werner, K. Glantschnig, C. Ambrosch-Draxl. Optical constants and inelastic electron-scattering data for 17 - elemental metals, J. Phys Chem Ref. Data 38, 1013-1092 (2009) + elemental metals, J. Phys Chem Ref. Data 38 + 1j*1013-1092 (2009) Yttrium oxide ("Y2O3") @@ -995,7 +995,7 @@ * Horiba Technical Note 08: Lorentz Dispersion Model `[pdf] `_. * Y. Nigara. Measurement of the optical constants of yttrium oxide, - Jpn. J. Appl. Phys. 7, 404-408 (1968) + Jpn. J. Appl. Phys. 7 + 1j*404-408 (1968) Yttrium aluminium garnet ("YAG") @@ -1016,7 +1016,7 @@ * D. E. Zelmon, D. L. Small and R. Page. Refractive-index measurements of undoped yttrium aluminum garnet - from 0.4 to 5.0 um, Appl. Opt. 37, 4933-4935 (1998) + from 0.4 to 5.0 um, Appl. Opt. 37 + 1j*4933-4935 (1998) Zirconium oxide ("ZrO2") @@ -1083,14 +1083,14 @@ 300K including temperature coefficients, Sol. Energ. Mat. Sol. Cells 92, 1305–1310 (2008). * M. A. Green and M. Keevers, Optical properties of intrinsic silicon - at 300 K, Progress in Photovoltaics, 3, 189-92 (1995). + at 300 K, Progress in Photovoltaics + 1j*3 + 1j*189-92 (1995). * H. H. Li. Refractive index of silicon and germanium and its wavelength - and temperature derivatives, J. Phys. Chem. Ref. Data 9, 561-658 (1993). + and temperature derivatives, J. Phys. Chem. Ref. Data 9 + 1j*561-658 (1993). * C. D. Salzberg and J. J. Villa. Infrared Refractive Indexes of Silicon, Germanium and Modified Selenium Glass, - J. Opt. Soc. Am., 47, 244-246 (1957). + J. Opt. Soc. Am. + 1j*47 + 1j*244-246 (1957). * B. Tatian. Fitting refractive-index data with the Sellmeier dispersion - formula, Appl. Opt. 23, 4477-4485 (1984). + formula, Appl. Opt. 23 + 1j*4477-4485 (1984). """ @@ -1099,28 +1099,28 @@ eps_inf=1.0, poles=[ ( - (-275580863813647.1, 312504541922578.7), - (410592688830514.8, -1.3173437570517746e16), + (-275580863813647.1 + 1j * 312504541922578.7), + (410592688830514.8 - 1j * 1.3173437570517746e16), ), ( - (-1148310840598705.2, 8055992835194972.0), - (227736607453638.5, -1042414461766764.9), + (-1148310840598705.2 + 1j * 8055992835194972.0), + (227736607453638.5 - 1j * 1042414461766764.9), ), ( - (-381116695232772.56, 6594145937912653.0), - (161555291564323.06, -1397161265004318.2), + (-381116695232772.56 + 1j * 6594145937912653.0), + (161555291564323.06 - 1j * 1397161265004318.2), ), ( - (-1.2755935758322332e16, 4213421975115564.5), - (1.718968422861484e16, 2.293341935281984e16), + (-1.2755935758322332e16 + 1j * 4213421975115564.5), + (1.718968422861484e16 + 1j * 2.293341935281984e16), ), ( - (-1037538194.0633082, -71105682833114.89), - (117311511.37080565, 6.61015554492372e17), + (-1037538194.0633082 - 1j * 71105682833114.89), + (117311511.37080565 + 1j * 6.61015554492372e17), ), ( - (-76642436669493.88, 123745349008080.44), - (129838572187083.62, -2.1821880909947117e17), + (-76642436669493.88 + 1j * 123745349008080.44), + (129838572187083.62 - 1j * 2.1821880909947117e17), ), ], frequency_range=(151926744799612.75, 7596337239980637.0), @@ -1130,17 +1130,17 @@ eps_inf=1.0, poles=[ ( - (-1.2332423729774158e16, -1157025502703526.8), - (1.0800083435396464e16, -4.781815206914558e16), + (-1.2332423729774158e16 - 1j * 1157025502703526.8), + (1.0800083435396464e16 - 1j * 4.781815206914558e16), ), ( - (-2229555965713773.2, -6952870039573486.0), - (4439804688475990.0, 6272392738308416.0), + (-2229555965713773.2 - 1j * 6952870039573486.0), + (4439804688475990.0 + 1j * 6272392738308416.0), ), - ((-7.482270443496804e-294, -528948840665300.7), (-0.0, -1.2076416298344678e17)), + ((-7.482270443496804e-294 - 1j * 528948840665300.7), (-0.0 - 1j * 1.2076416298344678e17)), ( - (-3295983388.845004, 314479339729201.94), - (7864861845440.258, -5.2524694748286035e17), + (-3295983388.845004 + 1j * 314479339729201.94), + (7864861845440.258 - 1j * 5.2524694748286035e17), ), ], frequency_range=(154771532566312.25, 1595489401708072.2), @@ -1150,21 +1150,21 @@ eps_inf=1.0, poles=[ ( - (-38634980988505.31, -48273958812026.45), - (4035140886647080.0, 2.835977690098632e18), + (-38634980988505.31 - 1j * 48273958812026.45), + (4035140886647080.0 + 1j * 2.835977690098632e18), ), - ((-1373449221156.457, 0.0), (7.630343339215653e16, 2.252091523762478e17)), + ((-1373449221156.457 + 1j * 0.0), (7.630343339215653e16 + 1j * 2.252091523762478e17)), ( - (-1.0762187388103686e16, -799978314126058.1), - (-1.5289438747838848e16, 4.746731963865045e16), + (-1.0762187388103686e16 - 1j * 799978314126058.1), + (-1.5289438747838848e16 + 1j * 4.746731963865045e16), ), ( - (-179338332256147.1, -243607346238054.5), - (-4.625363670034073e16, 7.703073947098675e16), + (-179338332256147.1 - 1j * 243607346238054.5), + (-4.625363670034073e16 + 1j * 7.703073947098675e16), ), ( - (-1.0180997365823526e16, -5542555481403632.0), - (-1.6978040336362288e16, -1.4140848316870884e16), + (-1.0180997365823526e16 - 1j * 5542555481403632.0), + (-1.6978040336362288e16 - 1j * 1.4140848316870884e16), ), ], frequency_range=(151926744799612.75, 1.5192674479961274e16), @@ -1172,46 +1172,46 @@ Al2O3_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.856240967961668e16), (0.0, 1.4107431356508676e16))], + poles=[((-0.0 - 1j * 1.856240967961668e16), (0.0 + 1j * 1.4107431356508676e16))], frequency_range=(145079354536315.6, 1450793545363156.0), ) AlAs_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-287141547671268.06, -6859562349716031.0), (0.0, 2.4978200955702556e16))], + poles=[((-287141547671268.06 - 1j * 6859562349716031.0), (0.0 + 1j * 2.4978200955702556e16))], frequency_range=(0.0, 725396772681578.0), ) AlAs_FernOnton1971 = PoleResidue( eps_inf=1, poles=[ - ((0.0, 6674881541314847.0), (-0.0, -2.0304989648679764e16)), - ((0.0, 68198825885555.74), (-0.0, -64788884591277.95)), + ((0.0 + 1j * 6674881541314847.0), (-0.0 - 1j * 2.0304989648679764e16)), + ((0.0 + 1j * 68198825885555.74), (-0.0 - 1j * 64788884591277.95)), ], frequency_range=(136269299354975.81, 535343676037405.0), ) AlGaN_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-96473482947754.08, -1.0968686723518324e16), (0.0, 1.974516343551917e16))], + poles=[((-96473482947754.08 - 1j * 1.0968686723518324e16), (0.0 + 1j * 1.974516343551917e16))], frequency_range=(145079354536315.6, 967195696908770.8), ) AlN_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.354578856633347e16), (0.0, 2.2391188500149228e16))], + poles=[((-0.0 - 1j * 1.354578856633347e16), (0.0 + 1j * 2.2391188500149228e16))], frequency_range=(181349193170394.5, 1148544890079165.2), ) AlxOy_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-654044636362332.8, -1.9535949662203744e16), (0.0, 2.123004231270711e16))], + poles=[((-654044636362332.8 - 1j * 1.9535949662203744e16), (0.0 + 1j * 2.123004231270711e16))], frequency_range=(145079354536315.6, 1450793545363156.0), ) Aminoacid_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -2.2518582114198596e16), (0.0, 5472015453750259.0))], + poles=[((-0.0 - 1j * 2.2518582114198596e16), (0.0 + 1j * 5472015453750259.0))], frequency_range=(362698386340789.0, 1208994621135963.5), ) @@ -1219,28 +1219,28 @@ eps_inf=1.0, poles=[ ( - (-2734662976094585.0, -5109708411015428.0), - (6336826024756207.0, 4435873101906770.0), + (-2734662976094585.0 - 1j * 5109708411015428.0), + (6336826024756207.0 + 1j * 4435873101906770.0), ), ( - (-1350147983711818.5, -5489311548525578.0), - (1313699470597296.0, 2519572763961442.0), + (-1350147983711818.5 - 1j * 5489311548525578.0), + (1313699470597296.0 + 1j * 2519572763961442.0), ), ( - (-617052918383578.8, -4245316498596240.5), - (577794256452581.6, 1959978954055246.2), + (-617052918383578.8 - 1j * 4245316498596240.5), + (577794256452581.6 + 1j * 1959978954055246.2), ), ( - (-49323313828269.45, 357801380626459.0), - (107506676273403.77, -1.4556042795341494e17), + (-49323313828269.45 + 1j * 357801380626459.0), + (107506676273403.77 - 1j * 1.4556042795341494e17), ), ( - (-1443242886602454.5, 1.2515133019565118e16), - (230166586216985.78, -3809468920144284.5), + (-1443242886602454.5 + 1j * 1.2515133019565118e16), + (230166586216985.78 - 1j * 3809468920144284.5), ), ( - (-258129278193.38495, 126209156799910.83), - (972898514880373.2, -2.6164309961808477e17), + (-258129278193.38495 + 1j * 126209156799910.83), + (972898514880373.2 - 1j * 2.6164309961808477e17), ), ], frequency_range=(972331166717521.5, 1.002716515677444e16), @@ -1249,9 +1249,9 @@ BK7_Zemax = PoleResidue( eps_inf=1, poles=[ - ((0.0, 2.431642149296798e16), (-0.0, -1.2639823249559002e16)), - ((0.0, 1.3313466757556814e16), (-0.0, -1542979833250087.0)), - ((0.0, 185098620483566.44), (-0.0, -93518250617894.06)), + ((0.0 + 1j * 2.431642149296798e16), (-0.0 - 1j * 1.2639823249559002e16)), + ((0.0 + 1j * 1.3313466757556814e16), (-0.0 - 1j * 1542979833250087.0)), + ((0.0 + 1j * 185098620483566.44), (-0.0 - 1j * 93518250617894.06)), ], frequency_range=(119916983432378.72, 999308195269822.8), ) @@ -1260,20 +1260,20 @@ eps_inf=1.0, poles=[ ( - (-1895389650993988.8, 97908760254751.03), - (40119229416830.445, -6.072472443146835e17), + (-1895389650993988.8 + 1j * 97908760254751.03), + (40119229416830.445 - 1j * 6.072472443146835e17), ), ( - (-173563254483411.3, -39098441331858.36), - (17327582796970.727, 2.1782706819526035e17), + (-173563254483411.3 - 1j * 39098441331858.36), + (17327582796970.727 + 1j * 2.1782706819526035e17), ), ( - (-3894265931723855.5, 4182034916796805.5), - (12304771601918.207, -7.207815056419813e16), + (-3894265931723855.5 + 1j * 4182034916796805.5), + (12304771601918.207 - 1j * 7.207815056419813e16), ), ( - (-21593264136101.0, 15791763527.314959), - (10898385976899.773, -1.844312751315413e21), + (-21593264136101.0 + 1j * 15791763527.314959), + (10898385976899.773 - 1j * 1.844312751315413e21), ), ], frequency_range=(30385348959922.547, 7596337239980637.0), @@ -1281,13 +1281,13 @@ CaF2_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -2.376134288665943e16), (0.0, 1.2308375615289586e16))], + poles=[((-0.0 - 1j * 2.376134288665943e16), (0.0 + 1j * 1.2308375615289586e16))], frequency_range=(181349193170394.5, 1148544890079165.2), ) Cellulose_Sultanova2009 = PoleResidue( eps_inf=1, - poles=[((0.0, 1.7889308287957964e16), (-0.0, -1.0053791257832376e16))], + poles=[((0.0 + 1j * 1.7889308287957964e16), (-0.0 - 1j * 1.0053791257832376e16))], frequency_range=(284973819943865.75, 686338046201801.2), ) @@ -1295,20 +1295,20 @@ eps_inf=1.0, poles=[ ( - (-1986166383636938.8, -2164878977347264.2), - (7556808013710.747, 7.049099034302554e16), + (-1986166383636938.8 - 1j * 2164878977347264.2), + (7556808013710.747 + 1j * 7.049099034302554e16), ), ( - (-721541271079502.1, -373401161923.8366), - (310196803320813.3, 3.9059060187608424e19), + (-721541271079502.1 - 1j * 373401161923.8366), + (310196803320813.3 + 1j * 3.9059060187608424e19), ), ( - (-63813936856379.42, -74339943925.90295), - (9692153948376.459, 1.677574997330204e20), + (-63813936856379.42 - 1j * 74339943925.90295), + (9692153948376.459 + 1j * 1.677574997330204e20), ), ( - (-14969882528204.193, 2792246309026.462), - (1365296575589394.2, -3.587733271017399e18), + (-14969882528204.193 + 1j * 2792246309026.462), + (1365296575589394.2 - 1j * 3.587733271017399e18), ), ], frequency_range=(151926744799612.75, 1.5192674479961274e16), @@ -1318,24 +1318,24 @@ eps_inf=1.0, poles=[ ( - (-26648472832094.61, -138613399508745.61), - (1569506577450794.8, 5.4114978936556614e17), + (-26648472832094.61 - 1j * 138613399508745.61), + (1569506577450794.8 + 1j * 5.4114978936556614e17), ), ( - (-371759347003379.5, -246275957923571.7), - (-3214099365675777.0, 6.815369975824028e16), + (-371759347003379.5 - 1j * 246275957923571.7), + (-3214099365675777.0 + 1j * 6.815369975824028e16), ), ( - (-729831805397277.0, -3688510464653965.0), - (1975278935189313.2, 3073498774961688.5), + (-729831805397277.0 - 1j * 3688510464653965.0), + (1975278935189313.2 + 1j * 3073498774961688.5), ), ( - (-3181433040973120.0, -6135291322604277.0), - (5089000024526812.0, 1.2704443456133342e16), + (-3181433040973120.0 - 1j * 6135291322604277.0), + (5089000024526812.0 + 1j * 1.2704443456133342e16), ), ( - (-40088932206916.91, -2.91706942364891e16), - (1249236469534085.0, 8344554643332125.0), + (-40088932206916.91 - 1j * 2.91706942364891e16), + (1249236469534085.0 + 1j * 8344554643332125.0), ), ], frequency_range=(972331166717521.5, 1.002716515677444e16), @@ -1344,9 +1344,9 @@ FusedSilica_Zemax = PoleResidue( eps_inf=1, poles=[ - ((0.0, 2.7537034527932452e16), (-0.0, -9585177720141492.0)), - ((0.0, 1.620465316968868e16), (-0.0, -3305284173070520.5)), - ((0.0, 190341645710801.38), (-0.0, -85413852993771.3)), + ((0.0 + 1j * 2.7537034527932452e16), (-0.0 - 1j * 9585177720141492.0)), + ((0.0 + 1j * 1.620465316968868e16), (-0.0 - 1j * 3305284173070520.5)), + ((0.0 + 1j * 190341645710801.38), (-0.0 - 1j * 85413852993771.3)), ], frequency_range=(44745143071783.1, 1427583136099746.8), ) @@ -1354,9 +1354,9 @@ GaAs_Skauli2003 = PoleResidue( eps_inf=1, poles=[ - ((0.0, 4250781024557878.5), (-0.0, -1.1618961579876792e16)), - ((0.0, 2153617667595138.0), (-0.0, -26166023937747.41)), - ((0.0, 51024513930292.87), (-0.0, -49940804278927.375)), + ((0.0 + 1j * 4250781024557878.5), (-0.0 - 1j * 1.1618961579876792e16)), + ((0.0 + 1j * 2153617667595138.0), (-0.0 - 1j * 26166023937747.41)), + ((0.0 + 1j * 51024513930292.87), (-0.0 - 1j * 49940804278927.375)), ], frequency_range=(17634850504761.58, 309064390289635.9), ) @@ -1364,54 +1364,56 @@ Ge_Icenogle1976 = PoleResidue( eps_inf=1, poles=[ - ((0.0, 2836329349380603.5), (-0.0, -9542546463056102.0)), - ((0.0, 30278857121656.766), (-0.0, -3225758043455.7036)), + ((0.0 + 1j * 2836329349380603.5), (-0.0 - 1j * 9542546463056102.0)), + ((0.0 + 1j * 30278857121656.766), (-0.0 - 1j * 3225758043455.7036)), ], frequency_range=(24982704881745.566, 119916983432378.72), ) GeOx_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-351710414211103.44, -2.4646085673376252e16), (0.0, 2.02755336442934e16))], + poles=[((-351710414211103.44 - 1j * 2.4646085673376252e16), (0.0 + 1j * 2.02755336442934e16))], frequency_range=(145079354536315.6, 967195696908770.8), ) H2O_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.7289263558195928e16), (0.0, 5938862032240302.0))], + poles=[((-0.0 - 1j * 1.7289263558195928e16), (0.0 + 1j * 5938862032240302.0))], frequency_range=(362698386340789.0, 1450793545363156.0), ) HMDS_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-379816861999031.8, -1.8227252520914852e16), (0.0, 1.0029341899480378e16))], + poles=[((-379816861999031.8 - 1j * 1.8227252520914852e16), (0.0 + 1j * 1.0029341899480378e16))], frequency_range=(362698386340789.0, 1571693007476752.5), ) HfO2_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-2278901171994190.5, -1.4098114301144558e16), (0.0, 1.3743164680834702e16))], + poles=[ + ((-2278901171994190.5 - 1j * 1.4098114301144558e16), (0.0 + 1j * 1.3743164680834702e16)) + ], frequency_range=(362698386340789.0, 1450793545363156.0), ) ITO_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-483886682186766.56, -1.031968022520672e16), (0.0, 1.292796190658882e16))], + poles=[((-483886682186766.56 - 1j * 1.031968022520672e16), (0.0 + 1j * 1.292796190658882e16))], frequency_range=(362698386340789.0, 1450793545363156.0), ) InP_Pettit1965 = PoleResidue( eps_inf=1, poles=[ - ((0.0, 3007586733129570.0), (-0.0, -3482785436964042.0)), - ((0.0, 57193003520845.59), (-0.0, -79069327367569.03)), + ((0.0 + 1j * 3007586733129570.0), (-0.0 - 1j * 3482785436964042.0)), + ((0.0 + 1j * 57193003520845.59), (-0.0 - 1j * 79069327367569.03)), ], frequency_range=(29979245858094.68, 315571009032575.6), ) MgF2_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -2.5358092974503356e16), (0.0, 1.1398462792039258e16))], + poles=[((-0.0 - 1j * 2.5358092974503356e16), (0.0 + 1j * 1.1398462792039258e16))], frequency_range=(193439139381754.16, 918835912063332.1), ) @@ -1419,16 +1421,16 @@ eps_inf=1.0, poles=[ ( - (-56577071909034.84, 1.709097252165159e16), - (104656337098134.19, -1.5807476741024398e16), + (-56577071909034.84 + 1j * 1.709097252165159e16), + (104656337098134.19 - 1j * 1.5807476741024398e16), ), ( - (-1.4437966258192067e17, -2258757151354688.5), - (1.5132011505098516e16, -4.810654072512032e17), + (-1.4437966258192067e17 - 1j * 2258757151354688.5), + (1.5132011505098516e16 - 1j * 4.810654072512032e17), ), ( - (-982824644.4296285, -4252237346494.8228), - (338287950556.00256, 4386571425642974.0), + (-982824644.4296285 - 1j * 4252237346494.8228), + (338287950556.00256 + 1j * 4386571425642974.0), ), ], frequency_range=(55517121959434.59, 832756829391519.0), @@ -1438,24 +1440,24 @@ eps_inf=1.0, poles=[ ( - (-130147997.31788255, -149469760922412.1), - (74748038596353.97, 3.01022049985022e17), + (-130147997.31788255 - 1j * 149469760922412.1), + (74748038596353.97 + 1j * 3.01022049985022e17), ), ( - (-27561493423510.0, -165502078583657.34), - (8080361635535756.0, -1.8948337145713684e16), + (-27561493423510.0 - 1j * 165502078583657.34), + (8080361635535756.0 - 1j * 1.8948337145713684e16), ), ( - (-226806637902024.8, -346391867988.41425), - (1.238514968044484e16, -1.3261156707711676e16), + (-226806637902024.8 - 1j * 346391867988.41425), + (1.238514968044484e16 - 1j * 1.3261156707711676e16), ), ( - (-980995274941083.2, -912202488656228.9), - (-898785384166810.4, 2.414339979079635e16), + (-980995274941083.2 - 1j * 912202488656228.9), + (-898785384166810.4 + 1j * 2.414339979079635e16), ), ( - (-4687205371459777.0, -8976520568647726.0), - (-5847989829468756.0, 8791690849762542.0), + (-4687205371459777.0 - 1j * 8976520568647726.0), + (-5847989829468756.0 + 1j * 8791690849762542.0), ), ], frequency_range=(972331166717521.5, 1.002716515677444e16), @@ -1463,42 +1465,42 @@ PEI_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.8231209375953524e16), (0.0, 9936009109894670.0))], + poles=[((-0.0 - 1j * 1.8231209375953524e16), (0.0 + 1j * 9936009109894670.0))], frequency_range=(181349193170394.5, 1148544890079165.2), ) PEN_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -6981033923542204.0), (0.0, 5117097865956436.0))], + poles=[((-0.0 - 1j * 6981033923542204.0), (0.0 + 1j * 5117097865956436.0))], frequency_range=(362698386340789.0, 773756557527016.6), ) PET_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.063487213597289e16), (0.0, 1.169835934957018e16))], + poles=[((-0.0 - 1j * 1.063487213597289e16), (0.0 + 1j * 1.169835934957018e16))], ) PMMA_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.7360669128251744e16), (0.0, 1.015599144002727e16))], + poles=[((-0.0 - 1j * 1.7360669128251744e16), (0.0 + 1j * 1.015599144002727e16))], frequency_range=(181349193170394.5, 1100185105233726.6), ) PMMA_Sultanova2009 = PoleResidue( eps_inf=1, - poles=[((0.0, 1.7709719337156064e16), (-0.0, -1.0465558642292376e16))], + poles=[((0.0 + 1j * 1.7709719337156064e16), (-0.0 - 1j * 1.0465558642292376e16))], frequency_range=(284973819943865.75, 686338046201801.2), ) PTFE_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -2.5039046810424176e16), (0.0, 8763666383648461.0))], + poles=[((-0.0 - 1j * 2.5039046810424176e16), (0.0 + 1j * 8763666383648461.0))], frequency_range=(362698386340789.0, 1571693007476752.5), ) PVC_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.8551774807480708e16), (0.0, 1.209575717447742e16))], + poles=[((-0.0 - 1j * 1.8551774807480708e16), (0.0 + 1j * 1.209575717447742e16))], frequency_range=(362698386340789.0, 1148544890079165.2), ) @@ -1506,21 +1508,21 @@ eps_inf=1.0, poles=[ ( - (-27947601188212.62, -88012749128378.45), - (-116820857784644.19, 4.431305747926611e17), + (-27947601188212.62 - 1j * 88012749128378.45), + (-116820857784644.19 + 1j * 4.431305747926611e17), ), - ((-42421241831450.59, 0.0), (2.0926917440899536e16, -2.322604734166214e17)), + ((-42421241831450.59 + 1j * 0.0), (2.0926917440899536e16 - 1j * 2.322604734166214e17)), ( - (-1156114791888924.0, -459830394883492.75), - (-2205692318269041.5, 5.882192811019071e16), + (-1156114791888924.0 - 1j * 459830394883492.75), + (-2205692318269041.5 + 1j * 5.882192811019071e16), ), ( - (-16850504828430.291, -19945795950186.92), - (-2244562993366961.8, 2.2399893428156035e17), + (-16850504828430.291 - 1j * 19945795950186.92), + (-2244562993366961.8 + 1j * 2.2399893428156035e17), ), ( - (-1.0165311890218712e16, -6195195244753680.0), - (-8682197716799510.0, -2496615613677907.5), + (-1.0165311890218712e16 - 1j * 6195195244753680.0), + (-8682197716799510.0 - 1j * 2496615613677907.5), ), ], frequency_range=(972331166717521.5, 1.002716515677444e16), @@ -1528,19 +1530,19 @@ Polycarbonate_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.8240324980641504e16), (0.0, 1.3716724385442412e16))], + poles=[((-0.0 - 1j * 1.8240324980641504e16), (0.0 + 1j * 1.3716724385442412e16))], frequency_range=(362698386340789.0, 967195696908770.8), ) Polycarbonate_Sultanova2009 = PoleResidue( eps_inf=1, - poles=[((0.0, 1.290535618305202e16), (-0.0, -9151188069402186.0))], + poles=[((0.0 + 1j * 1.290535618305202e16), (-0.0 - 1j * 9151188069402186.0))], frequency_range=(284973819943865.75, 686338046201801.2), ) Polystyrene_Sultanova2009 = PoleResidue( eps_inf=1, - poles=[((0.0, 1.3248080478547494e16), (-0.0, -9561802085391654.0))], + poles=[((0.0 + 1j * 1.3248080478547494e16), (-0.0 - 1j * 9561802085391654.0))], frequency_range=(284973819943865.75, 686338046201801.2), ) @@ -1548,24 +1550,24 @@ eps_inf=1.0, poles=[ ( - (-101718046412896.23, -222407105780688.0), - (4736075731111783.0, 7.146182537352074e17), + (-101718046412896.23 - 1j * 222407105780688.0), + (4736075731111783.0 + 1j * 7.146182537352074e17), ), ( - (-78076341531946.67, -60477052937666.555), - (5454987478240738.0, 4.413657205572709e17), + (-78076341531946.67 - 1j * 60477052937666.555), + (5454987478240738.0 + 1j * 4.413657205572709e17), ), ( - (-6487635330201033.0, -155489439108998.5), - (5343260155670645.0, 2.067963085430939e17), + (-6487635330201033.0 - 1j * 155489439108998.5), + (5343260155670645.0 + 1j * 2.067963085430939e17), ), ( - (-2281398148570798.5, -64631536899092.15), - (-1930595420879896.2, -4.8251418308161344e17), + (-2281398148570798.5 - 1j * 64631536899092.15), + (-1930595420879896.2 - 1j * 4.8251418308161344e17), ), ( - (-9967323231923196.0, -4041974141709040.5), - (-501748269346742.7, 6.883385112306915e16), + (-9967323231923196.0 - 1j * 4041974141709040.5), + (-501748269346742.7 + 1j * 6.883385112306915e16), ), ], frequency_range=(120884055879414.03, 2997924585809468.0), @@ -1573,76 +1575,76 @@ Sapphire_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -2.0143967092980652e16), (0.0, 2.105044561216478e16))], + poles=[((-0.0 - 1j * 2.0143967092980652e16), (0.0 + 1j * 2.105044561216478e16))], frequency_range=(362698386340789.0, 1329894083249559.8), ) Si3N4_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-1357465464784539.5, -4646140872332419.0), (0.0, 1.103606337254506e16))], + poles=[((-1357465464784539.5 - 1j * 4646140872332419.0), (0.0 + 1j * 1.103606337254506e16))], frequency_range=(362698386340789.0, 1329894083249559.8), ) Si3N4_Philipp1973 = PoleResidue( eps_inf=1, - poles=[((0.0, 1.348644355236665e16), (-0.0, -1.9514209498096924e16))], + poles=[((0.0 + 1j * 1.348644355236665e16), (-0.0 - 1j * 1.9514209498096924e16))], frequency_range=(241768111758828.06, 1448272746767859.0), ) Si3N4_Luke2015 = PoleResidue( eps_inf=1, poles=[ - ((0.0, 1.391786035350109e16), (-0.0, -2.1050067891652724e16)), - ((0.0, 1519267431623.5857), (-0.0, -3.0623873619236616e16)), + ((0.0 + 1j * 1.391786035350109e16), (-0.0 - 1j * 2.1050067891652724e16)), + ((0.0 + 1j * 1519267431623.5857), (-0.0 - 1j * 3.0623873619236616e16)), ], frequency_range=(54468106573573.19, 967072447035312.2), ) SiC_Horiba = PoleResidue( eps_inf=3.0, - poles=[((-0.0, -1.2154139583969018e16), (0.0, 2.3092865209541132e16))], + poles=[((-0.0 - 1j * 1.2154139583969018e16), (0.0 + 1j * 2.3092865209541132e16))], frequency_range=(145079354536315.6, 967195696908770.8), ) SiN_Horiba = PoleResidue( eps_inf=2.32, - poles=[((-302334222151229.3, -9863009385232968.0), (0.0, 6244215164693547.0))], + poles=[((-302334222151229.3 - 1j * 9863009385232968.0), (0.0 + 1j * 6244215164693547.0))], frequency_range=(145079354536315.6, 1450793545363156.0), ) SiO2_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-75963372399806.36, -1.823105111824081e16), (0.0, 1.0209565875622414e16))], + poles=[((-75963372399806.36 - 1j * 1.823105111824081e16), (0.0 + 1j * 1.0209565875622414e16))], frequency_range=(169259246959034.88, 1208994621135963.5), ) SiON_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.651139862482191e16), (0.0, 1.1079148477255502e16))], + poles=[((-0.0 - 1j * 1.651139862482191e16), (0.0 + 1j * 1.1079148477255502e16))], frequency_range=(181349193170394.5, 725396772681578.0), ) Ta2O5_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-618341851334423.8, -1.205777404193952e16), (0.0, 1.8938176054079756e16))], + poles=[((-618341851334423.8 - 1j * 1.205777404193952e16), (0.0 + 1j * 1.8938176054079756e16))], frequency_range=(181349193170394.5, 967195696908770.8), ) Ti_Werner2009 = PoleResidue( eps_inf=1.0, poles=[ - ((-55002727357489.695, -103457301057900.64), (0.0, 1.4157836508658926e18)), - ((-3889516074161299.0, -6.314261108475189e16), (0.0, 2192302508847248.2)), - ((-2919746613155850.5, -7.211858151732786e16), (0.0, 744301222539582.0)), - ((-4635394958195360.0, -5.622429893839941e16), (0.0, 2101343798471838.0)), - ((-9774364062177540.0, -4844300045008988.0), (0.0, 7.377824793744533e16)), + ((-55002727357489.695 - 1j * 103457301057900.64), (0.0 + 1j * 1.4157836508658926e18)), + ((-3889516074161299.0 - 1j * 6.314261108475189e16), (0.0 + 1j * 2192302508847248.2)), + ((-2919746613155850.5 - 1j * 7.211858151732786e16), (0.0 + 1j * 744301222539582.0)), + ((-4635394958195360.0 - 1j * 5.622429893839941e16), (0.0 + 1j * 2101343798471838.0)), + ((-9774364062177540.0 - 1j * 4844300045008988.0), (0.0 + 1j * 7.377824793744533e16)), ], frequency_range=(120884055879414.03, 2997924585809468.0), ) TiOx_Horiba = PoleResidue( eps_inf=0.29, - poles=[((-0.0, -9875238411974826.0), (0.0, 1.7429795797135566e16))], + poles=[((-0.0 - 1j * 9875238411974826.0), (0.0 + 1j * 1.7429795797135566e16))], frequency_range=(145079354536315.6, 725396772681578.0), ) @@ -1650,24 +1652,24 @@ eps_inf=1.0, poles=[ ( - (-6008545281436.0, -273822982315836.25), - (2874701466157776.0, 6.354855141434104e17), + (-6008545281436.0 - 1j * 273822982315836.25), + (2874701466157776.0 + 1j * 6.354855141434104e17), ), ( - (-18716635733325.97, -7984905262277.852), - (2669048417776342.0, 1.4111869583971584e17), + (-18716635733325.97 - 1j * 7984905262277.852), + (2669048417776342.0 + 1j * 1.4111869583971584e17), ), ( - (-7709052771634303.0, -64340875428723.28), - (501889387931716.2, 5.510078120444142e16), + (-7709052771634303.0 - 1j * 64340875428723.28), + (501889387931716.2 + 1j * 5.510078120444142e16), ), ( - (-330546522884264.1, -1422878310689065.0), - (584859595267922.1, 3.664402566039364e16), + (-330546522884264.1 - 1j * 1422878310689065.0), + (584859595267922.1 + 1j * 3.664402566039364e16), ), ( - (-3989296857299139.0, -3986090497375137.0), - (-352374832782093.06, 6.323677441887342e16), + (-3989296857299139.0 - 1j * 3986090497375137.0), + (-352374832782093.06 + 1j * 6.323677441887342e16), ), ], frequency_range=(120884055879414.03, 2997924585809468.0), @@ -1675,15 +1677,15 @@ Y2O3_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-0.0, -1.3814698904628784e16), (0.0, 1.1846104310719182e16))], + poles=[((-0.0 - 1j * 1.3814698904628784e16), (0.0 + 1j * 1.1846104310719182e16))], frequency_range=(374788332552148.7, 967195696908770.8), ) Y2O3_Nigara1968 = PoleResidue( eps_inf=1, poles=[ - ((0.0, 1.3580761146063806e16), (-0.0, -1.7505601117276244e16)), - ((0.0, 82126420080181.8), (-0.0, -161583731507757.7)), + ((0.0 + 1j * 1.3580761146063806e16), (-0.0 - 1j * 1.7505601117276244e16)), + ((0.0 + 1j * 82126420080181.8), (-0.0 - 1j * 161583731507757.7)), ], frequency_range=(31228381102181.96, 1199169834323787.2), ) @@ -1691,35 +1693,35 @@ YAG_Zelmon1998 = PoleResidue( eps_inf=1, poles=[ - ((0.0, 1.7303796419562446e16), (-0.0, -1.974363171472075e16)), - ((0.0, 112024123195387.16), (-0.0, -183520159101147.16)), + ((0.0 + 1j * 1.7303796419562446e16), (-0.0 - 1j * 1.974363171472075e16)), + ((0.0 + 1j * 112024123195387.16), (-0.0 - 1j * 183520159101147.16)), ], frequency_range=(59958491716189.36, 749481146452367.0), ) ZrO2_Horiba = PoleResidue( eps_inf=1.0, - poles=[((-97233116671752.14, -1.446765717253359e16), (0.0, 2.0465425413547396e16))], + poles=[((-97233116671752.14 - 1j * 1.446765717253359e16), (0.0 + 1j * 2.0465425413547396e16))], frequency_range=(362698386340789.0, 725396772681578.0), ) aSi_Horiba = PoleResidue( eps_inf=3.109, - poles=[((-1458496750076282.0, -5789844327200831.0), (0.0, 4.485863370051096e16))], + poles=[((-1458496750076282.0 - 1j * 5789844327200831.0), (0.0 + 1j * 4.485863370051096e16))], frequency_range=(362698386340789.0, 1450793545363156.0), ) cSi_SalzbergVilla1957 = PoleResidue( eps_inf=1.0, - poles=[((0.0, 6206417594288582.0), (-0.0, -3.311074436985222e16))], + poles=[((0.0 + 1j * 6206417594288582.0), (-0.0 - 1j * 3.311074436985222e16))], frequency_range=(27253859870995.164, 220435631309519.7), ) cSi_Li1993_293K = PoleResidue( eps_inf=1.0, poles=[ - ((0.0, 4010819041318578.0), (0.0, 1.2156273362672036e16)), - ((0.0, 5022626939326166.0), (-0.0, -4.1977794227247144e16)), + ((0.0 + 1j * 4010819041318578.0), (0.0 + 1j * 1.2156273362672036e16)), + ((0.0 + 1j * 5022626939326166.0), (-0.0 - 1j * 4.1977794227247144e16)), ], frequency_range=(21413747041496.2, 249827048817455.7), ) @@ -1728,20 +1730,20 @@ eps_inf=1.0, poles=[ ( - (-516580533476358.94, -7988869406082532.0), - (531784950915900.1, 4114144409090735.5), + (-516580533476358.94 - 1j * 7988869406082532.0), + (531784950915900.1 + 1j * 4114144409090735.5), ), ( - (-422564506478804.25, -6388843514992565.0), - (2212987364690094.5, 1.665883190033301e16), + (-422564506478804.25 - 1j * 6388843514992565.0), + (2212987364690094.5 + 1j * 1.665883190033301e16), ), ( - (-169315596364414.94, 5194420450502291.0), - (301374428182025.6, -4618167601749804.0), + (-169315596364414.94 + 1j * 5194420450502291.0), + (301374428182025.6 - 1j * 4618167601749804.0), ), ( - (-379444981070553.4, 5656363945615038.0), - (1105733518717537.1, -8204725853411607.0), + (-379444981070553.4 + 1j * 5656363945615038.0), + (1105733518717537.1 - 1j * 8204725853411607.0), ), ], frequency_range=(206753419710997.8, 1199169834323787.2), diff --git a/tidy3d/plugins/dispersion/fit.py b/tidy3d/plugins/dispersion/fit.py index b895a2c20c..32df967660 100644 --- a/tidy3d/plugins/dispersion/fit.py +++ b/tidy3d/plugins/dispersion/fit.py @@ -253,8 +253,7 @@ def _make_medium(self, coeffs): Dispersive medium corresponding to this set of ``coeffs``. """ poles_complex = _coeffs_to_poles(coeffs) - poles_re_im = [(_unpack_complex(a), _unpack_complex(c)) for (a, c) in poles_complex] - return PoleResidue(poles=poles_re_im, frequency_range=self.frequency_range) + return PoleResidue(poles=poles_complex, frequency_range=self.frequency_range) def fit_single( self, diff --git a/tidy3d/web/auth.py b/tidy3d/web/auth.py index 3d5084fdad..4a606dc8a5 100644 --- a/tidy3d/web/auth.py +++ b/tidy3d/web/auth.py @@ -39,9 +39,9 @@ def set_authentication_config(email: str, password: str) -> None: def get_credentials() -> None: - """what""" + """Tries to log user in from file, if not working, prompts user for login info and saves.""" - # if we find both email and password in the credential path + # if we find something in the credential path if os.path.exists(credential_path): # try to authenticate them diff --git a/tidy3d/web/container.py b/tidy3d/web/container.py index eb73f10e6f..e8ea3db1e2 100644 --- a/tidy3d/web/container.py +++ b/tidy3d/web/container.py @@ -19,11 +19,11 @@ class WebContainer(Tidy3dBaseModel, ABC): - """base class for job and batch, technically not used""" + """Base class for :class:`Job` and :class:`Batch`, technically not used""" class Job(WebContainer): - """Interface for managing the running of a :class:`Simulation` on server.""" + """Interface for managing the running of a :class:`.Simulation` on server.""" simulation: Simulation task_name: TaskName @@ -35,13 +35,13 @@ def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: Parameters ---------- - path_dir : str + path_dir : str = "./simulation_data.hdf5" Base directory where data will be downloaded, by default current working directory. Returns ------- - ``{TaskName: SimulationData}`` - Dictionary mapping task name to :class:`SimulationData` for :class:`Job`. + Dict[str: :class:`.SimulationData`] + Dictionary mapping task name to :class:`.SimulationData` for :class:`Job`. """ self.upload() @@ -50,7 +50,12 @@ def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: return self.load_data(path=path) def upload(self) -> None: - """Upload simulation to server without running.""" + """Upload simulation to server without running. + + Note + ---- + To start the simulation running, call :meth:`Job.start` after uploaded. + """ task_id = web.upload( simulation=self.simulation, task_name=self.task_name, folder_name=self.folder_name ) @@ -61,34 +66,46 @@ def get_info(self) -> TaskInfo: Returns ------- - ``TaskInfo`` + :class:`TaskInfo` :class:`TaskInfo` object containing info about status, size, credits of task and others. """ + task_info = web.get_info(task_id=self.task_id) return task_info @property def status(self): - """return current status""" + """Return current status of :class:`Job`.""" return self.get_info().status def start(self) -> None: - """start running a task""" + """Start running a :class:`Job`. + + Note + ---- + To monitor progress of the :class:`Job`, call :meth:`Job.monitor` after started. + """ web.start(self.task_id) def get_run_info(self) -> RunInfo: - """Return information about the running ``Job``. + """Return information about the running :class:`Job`. Returns ------- - RunInfo + :class:`RunInfo` Task run information. """ run_info = web.get_run_info(task_id=self.task_id) return run_info def monitor(self) -> None: - """monitor progress of running ``Job``.""" + """Monitor progress of running :class:`Job`. + + Note + ---- + To load the output of completed simulation into :class:`.SimulationData`objets, + call :meth:`Job.load_data`. + """ status = self.status console = Console() @@ -107,8 +124,12 @@ def download(self, path: str = DEFAULT_DATA_PATH) -> None: Parameters ---------- - path : ``str`` + path : str = "./simulation_data.hdf5" Path to download data as ``.hdf5`` file (including filename). + + Note + ---- + To load the data into :class:`.SimulationData`objets, can call :meth:`Job.load_data`. """ web.download(task_id=self.task_id, simulation=self.simulation, path=path) @@ -118,12 +139,12 @@ def load_data(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: Parameters ---------- - path : str + path : str = "./simulation_data.hdf5" Path to download data as ``.hdf5`` file (including filename). Returns ------- - :class:`SimulationData` + :class:`.SimulationData` Object containing data about simulation. """ return web.load_data(task_id=self.task_id, simulation=self.simulation, path=path) @@ -135,13 +156,13 @@ def delete(self): class Batch(WebContainer): - """Interface for submitting several :class:`Simulation` objects to sever. + """Interface for submitting several :class:`.Simulation` objects to sever. Parameters ---------- - simulations : ``{str: :class:`Simulation`}`` - Mapping of task name to :class:`Simulation` objects. - folder_name : ``str``, optional + simulations : Dict[str, :class:`.Simulation`] + Mapping of task name to :class:`.Simulation` objects. + folder_name : ``str`` = './' Name of folder to store member of each batch on web UI. """ @@ -150,17 +171,30 @@ class Batch(WebContainer): folder_name: str = "default" def run(self, path_dir: str = DEFAULT_DATA_DIR): - """Run each :class:`Job` in :class:`Batch` all the way through and return iterator for data. + """Upload and run each simulation in :class:`Batch`. + Returns generator that can be used to loop through data results. Parameters ---------- - path_dir : ``str`` + path_dir : str Base directory where data will be downloaded, by default current working directory. Yields ------ - ``(TaskName, SimulationData)`` - Task name and Simulation data, returned one by one if iterated over. + str, :class:`.SimulationData` + Yields the name of task + and its corresponding :class:`.SimulationData` at each iteration. + + Note + ---- + A typical usage might look like: + + >>> batch_results = batch.run() + >>> for task_name, sim_data in batch_results: + ... # do something with data. + + Note that because ``batch_results`` is a generator, only the current iteration of + :class:`.SimulationData` is stored in memory at a time. """ self.upload() @@ -170,7 +204,12 @@ def run(self, path_dir: str = DEFAULT_DATA_DIR): return self.items() def upload(self) -> None: - """create jobs and upload to server""" + """Create a series of tasks in the :class:`Batch` and upload them to server. + + Note + ---- + To start the simulations running, must call :meth:`Batch.start` after uploaded. + """ self.jobs = {} for task_name, simulation in self.simulations.items(): job = Job(simulation=simulation, task_name=task_name, folder_name=self.folder_name) @@ -178,12 +217,12 @@ def upload(self) -> None: job.upload() def get_info(self) -> Dict[TaskName, TaskInfo]: - """get general information about all job's task + """Get information about each task in the :class:`Batch`. Returns ------- - ``{str: :class:`TaskInfo`}`` - Description + Dict[str, :class:`TaskInfo`] + Mapping of task name to data about task associated with each task. """ info_dict = {} for task_name, job in self.jobs.items(): @@ -192,17 +231,22 @@ def get_info(self) -> Dict[TaskName, TaskInfo]: return info_dict def start(self) -> None: - """start running a task""" + """Start running all tasks in the :class:`Batch`. + + Note + ---- + To monitor the running simulations, can call :meth:`Batch.monitor`. + """ for _, job in self.jobs.items(): job.start() def get_run_info(self) -> Dict[TaskName, RunInfo]: - """get information about a each of the tasks in batch. + """get information about a each of the tasks in the :class:`Batch`. Returns ------- - ``{str: RunInfo}`` - Dictionary of task name to dictionary of run info for each task. + Dict[str: :class:`RunInfo`] + Maps task names to run info for each task in the :class:`Batch`. """ run_info_dict = {} for task_name, job in self.jobs.items(): @@ -211,7 +255,12 @@ def get_run_info(self) -> Dict[TaskName, RunInfo]: return run_info_dict def monitor(self) -> None: # pylint:disable=too-many-locals - """monitor progress of each of the running tasks in batch.""" + """Monitor progress of each of the running tasks. + + Note + ---- + To loop through the data of completed simulations, can call :meth:`Batch.items`. + """ def pbar_description(task_name: str, status: str) -> str: return f"{task_name}: status = {status}" @@ -248,50 +297,63 @@ def pbar_description(task_name: str, status: str) -> str: @staticmethod def _job_data_path(task_id: TaskId, path_dir: str = DEFAULT_DATA_DIR): - """Default path to data of a single Job in Batch + """Default path to data of a single :class:`Job` in :class:`Batch`. Parameters ---------- - task_id : ``TaskId`` - task_id corresponding to a :class:`Job` - path_dir : ``str``, optional - Base directory where data will be downloaded, by default current working directory. + task_id : str + task_id corresponding to a :class:`Job`. + path_dir : str = './' + Base directory where data will be downloaded, by default, the current working directory. Returns ------- str - path of the data file + Full path to the data file. """ return os.path.join(path_dir, f"{str(task_id)}.hdf5") def download(self, path_dir: str = DEFAULT_DATA_DIR) -> None: - """download results. + """Download results of each task. Parameters ---------- - path_dir : ``str`` - Base directory where data will be downloaded, by default current working directory. + path_dir : str = './' + Base directory where data will be downloaded, by default the current working directory. + + Note + ---- + To load the data into :class:`.SimulationData`objets, can call :meth:`Batch.items`. + + The data for each task will be named as ``{path_dir}/{task_name}.hdf5``. + """ + for task_name, job in self.jobs.items(): job_path = self._job_data_path(task_name, path_dir) job.download(path=job_path) def load_data(self, path_dir: str = DEFAULT_DATA_DIR) -> Dict[TaskName, SimulationData]: - """download results and load them into SimulationData object. - Note: this will return a dictionary of :class:`SimulationData` objects, each of which can - hold a large amount of data. - Use `Batch.items()` to instead loop through :class:`SimulationData` objects and only store - current iteration in memory if many simulations or large amounts of data. + """Download results and load them into :class:`.SimulationData` object. Parameters ---------- - path_dir : str + path_dir : str = './' Base directory where data will be downloaded, by default current working directory. Returns ------- - ``{TaskName: SimulationData}`` - Dictionary mapping task name to :class:`SimulationData` for :class:`Job`. + Dict[str, :class:`.SimulationData`] + Dictionary mapping task names to :class:`.SimulationData` for :class:`Batch`. + + Note + ---- + This will return a dictionary of :class:`.SimulationData` objects, + each of which can hold a large amount of data. + If many simulations or large amounts of data, + use ``for task_name, sim_data in Batch.items():`` + to instead loop through :class:`.SimulationData` objects and only store + current iteration in memory. """ sim_data_dir = {} self.download(path_dir=path_dir) @@ -302,23 +364,25 @@ def load_data(self, path_dir: str = DEFAULT_DATA_DIR) -> Dict[TaskName, Simulati return sim_data_dir def delete(self): - """delete server-side data associated with job""" + """Delete server-side data associated with each task in the batch.""" for _, job in self.jobs.items(): job.delete() self.jobs = None def items(self, path_dir: str = DEFAULT_DATA_DIR) -> Generator: - """simple iterator, ``for task_name, sim_data in batch.items(): do something`` + """Generates :class:`.SimulationData` for batch. + Used like: ``for task_name, sim_data in batch.items(): do something``. Parameters ---------- - path_dir : ``str`` + path_dir : str = './' Base directory where data will be downloaded, by default current working directory. Yields ------ - ``(TaskName, SimulationData)`` - Task name and Simulation data, returned one by one if iterated over. + str, :class:`.SimulationData` + Yields the name of task + and its corresponding :class:`.SimulationData` at each iteration. """ for task_name, job in self.jobs.items(): job_path = self._job_data_path(task_id=job.task_id, path_dir=path_dir) diff --git a/tidy3d/web/webapi.py b/tidy3d/web/webapi.py index 72a8311e41..4bd9f7dce3 100644 --- a/tidy3d/web/webapi.py +++ b/tidy3d/web/webapi.py @@ -19,11 +19,12 @@ from ..convert import export_old_json, load_old_monitor_data, load_solver_results +# TODO: Original simulation still needed in download functions because we don't convert +# old json files to new ones. + REFRESH_TIME = 0.3 TOTAL_DOTS = 3 -""" webapi functions """ - def run( simulation: Simulation, @@ -31,23 +32,24 @@ def run( folder_name: str = "default", path: str = "simulation_data.hdf5", ) -> SimulationData: - """submits simulation to server, starts running, monitors progress, downloads and loads results. + """Submits a :class:`.Simulation` to server, starts running, monitors progress, downloads, + and loads results as a :class:`.SimulationData` object. Parameters ---------- - simulation : :class:`Simulation` + simulation : :class:`.Simulation` Simulation to upload to server. - task_name : ``str`` - Name of task - path : ``str`` + task_name : str + Name of task. + path : str = "simulation_data.hdf5" Path to download results file (.hdf5), including filename. - folder_name : ``str`` - Name of folder to store task on web UI + folder_name : str = "default" + Name of folder to store task on web UI. Returns ------- - :class:`SimulationData` - Object containing solver results for the supplied :class:`Simulation`. + :class:`.SimulationData` + Object containing solver results for the supplied :class:`.Simulation`. """ task_id = upload(simulation=simulation, task_name=task_name, folder_name=folder_name) start(task_id) @@ -56,22 +58,25 @@ def run( def upload(simulation: Simulation, task_name: str, folder_name: str = "default") -> TaskId: - """upload simulation to server (as draft, dont run). + """Upload simulation to server, but do not start running :class:`.Simulation`. Parameters ---------- - simulation : :class:`Simulation` + simulation : :class:`.Simulation` Simulation to upload to server. - task_name : ``str`` - name of task - folder_name : ``str`` - name of folder to store task on web UI - + task_name : str + Name of task. + folder_name : str + Name of folder to store task on web UI Returns ------- TaskId Unique identifier of task on server. + + Note + ---- + To start the simulation running, must call :meth:`start` after uploaded. """ return _upload_task(simulation=simulation, task_name=task_name, folder_name=folder_name) @@ -81,12 +86,12 @@ def get_info(task_id: TaskId) -> TaskInfo: Parameters ---------- - task_id : TaskId - Unique identifier of task on server. + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. Returns ------- - TaskInfo + :class:`TaskInfo` Object containing information about status, size, credits of task. """ method = os.path.join("fdtd/task", task_id) @@ -101,8 +106,12 @@ def start(task_id: TaskId) -> None: Parameters ---------- - task_id : TaskId - Unique identifier of task on server. + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. + + Note + ---- + To monitor progress, can call :meth:`monitor` after starting simulation. """ task = get_info(task_id) folder_name = task.folderId @@ -112,12 +121,12 @@ def start(task_id: TaskId) -> None: def get_run_info(task_id: TaskId): - """gets the % done and field_decay for a running task + """Gets the % done and field_decay for a running task. Parameters ---------- - task_id : TaskId - Unique identifier of task on server. + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. Returns ------- @@ -140,8 +149,12 @@ def monitor(task_id: TaskId) -> None: Parameters ---------- - task_id : ``TaskId`` - Unique identifier of task on server. + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. + + Note + ---- + To load results when finished, may call :meth:`load_data`. """ task_info = get_info(task_id) @@ -198,13 +211,15 @@ def monitor(task_id: TaskId) -> None: def download(task_id: TaskId, simulation: Simulation, path: str = "simulation_data.hdf5") -> None: - """Fownload results of task and log to file. + """Download results of task and log to file. Parameters ---------- - task_id : TaskId - Unique identifier of task on server. - path : str + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. + simulation : :class:`.Simulation` + Original simulation. + path : str = "simulation_data.hdf5" Download path to .hdf5 data file (including filename). """ @@ -258,20 +273,22 @@ def load_data( path: str = "simulation_data.hdf5", replace_existing=True, ) -> SimulationData: - """Download and Load simultion results into ``SimulationData`` object. + """Download and Load simultion results into :class:`.SimulationData` object. Parameters ---------- - task_id : ``TaskId`` - Unique identifier of task on server. - path : ``str`` + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. + simulation : :class:`.Simulation` + Original simulation. + path : str Download path to .hdf5 data file (including filename). - replace_existing: ``bool`` - Downloads even if file exists (overwriting). + replace_existing: bool = True + Downloads even if file exists (overwriting the existing). Returns ------- - :class:`SimulationData` + :class:`.SimulationData` Object containing simulation data. """ if not os.path.exists(path) or replace_existing: @@ -286,8 +303,8 @@ def delete(task_id: TaskId) -> TaskInfo: Parameters ---------- - task_id : TaskId - Unique identifier of task on server. + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. Returns ------- @@ -313,7 +330,7 @@ def _upload_task( # pylint:disable=too-many-locals json_string = json.dumps(sim_dict, indent=4) # TODO: remove node size, time steps, compute weight, worker group - node_size = int(np.prod([len(sizes) for sizes in simulation.grid.cell_sizes.dict().values()])) + node_size = int(np.prod([len(sizes) for sizes in simulation.grid.sizes.dict().values()])) data = { "status": "draft", "solverVersion": solver_version, @@ -357,15 +374,15 @@ def _upload_task( # pylint:disable=too-many-locals def _download_file(task_id: TaskId, fname: str, path: str) -> None: - """Download a specific file ``fname`` to ``path``. + """Download a specific file from server. Parameters ---------- - task_id : ``TaskId`` - Unique identifier of task on server. - fname : ``str`` + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. + fname : str Name of the file on server (eg. ``monitor_data.hdf5``, ``tidy3d.log``, ``simulation.json``) - path : ``str`` + path : str Path where the file will be downloaded to (including filename). """ log.info(f'downloading file "{fname}" to "{path}"') @@ -397,7 +414,7 @@ def _download_file(task_id: TaskId, fname: str, path: str) -> None: def _rm_file(path: str): - """clear path if it exists""" + """Clear path if it exists.""" if os.path.exists(path) and not os.path.isdir(path): log.info(f"removing file {path}") os.remove(path)