diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 6332da6b2..0e7cb240b 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -15,11 +15,8 @@ jobs: runs-on: ${{ matrix.platform }} strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.8', '3.9', '3.10', '3.11'] platform: [ubuntu-latest, macos-latest, windows-latest] - exclude: - - platform: macos-latest - python-version: '3.7' steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 09ae036ba..a45c67b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,74 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - ## [Unreleased] +## [2.5.0] - 2023-12-13 + +### Added +- Ability to mix regular mediums and geometries with differentiable analogues in `JaxStructure`. Enables support for shape optimization with dispersive mediums. New classes `JaxStructureStaticGeometry` and `JaxStructureStaticMedium` accept regular `Tidy3D` geometry and medium classes, respectively. +- Warning if nonlinear mediums are used in an `adjoint` simulation. In this case, the gradients will not be accurate, but may be approximately correct if the nonlinearity is weak. +- Validator for surface field projection monitors that warns if projecting backwards relative to the monitor's normal direction. +- Validator for field projection monitors when far field approximation is enabled but the projection distance is small relative to the near field domain. +- Ability to manually specify a medium through which to project fields, when using field projection monitors. +- Added support for two-photon absorption via `TwoPhotonAbsorption` class. Added `KerrNonlinearity` that implements Kerr effect without third-harmonic generation. +- Can create `PoleResidue` from LO-TO form via `PoleResidue.from_lo_to`. +- Added `TriangularGridDataset` and `TehrahedralGridDataset` for storing and manipulating unstructured data. +- Support for an anisotropic medium containing PEC components. +- `SimulationData.mnt_data_from_file()` method to load only a single monitor data object from a simulation data `.hdf5` file. +- `_hash_self` to base model, uses `hashlib` to hash a Tidy3D component the same way every session. +- `ComponentModeler.plot_sim_eps()` method to plot the simulation permittivity and ports. +- Support for 2D PEC materials. +- Ability to downsample recorded near fields to speed up server-side far field projections. +- `FieldData.apply_phase(phase)` to multiply field data by a phase. +- Optional `phase` argument to `SimulationData.plot_field` that applies a phase to complex-valued fields. +- Ability to window near fields for spatial filtering of far fields for both client- and server-side field projections. +- Support for multiple frequencies in `output_monitors` in `adjoint` plugin. +- GDSII export functions `to_gds_file`, `to_gds`, `to_gdspy`, and `to_gdstk` to `Simulation`, `Structure`, and `Geometry`. +- `verbose` argument to `estimate_cost` and `real_cost` functions such that the cost is logged if `verbose==True` (default). Additional helpful messages may also be logged. +- Support for space-time modulation of permittivity and electric conductivity via `ModulationSpec` class. The modulation function must be separable in space and time. Modulations with user-supplied distributions in space and harmonic modulation in time are supported. +- `Geometry.intersections_tilted_plane` calculates intersections with any plane, not only axis-aligned ones. +- `Transformed` class to support geometry transformations. +- Methods `Geometry.translated`, `Geometry.scaled`, and `Geometry.rotated` can be used to create transformed copies of any geometry. +- Time zone in webAPI logging output. +- Class `Scene` consisting of a background medium and structures for easier drafting and visualization of simulation setups as well as transferring such information between different simulations. +- Solver for thermal simulation (see `HeatSimulation` and related classes). +- Specification of material thermal properties in medium classes through an optional field `.heat_spec`. + +### Changed +- Credit cost for remote mode solver has been modified to be defined in advance based on the mode solver details. Previously, the cost was based on elapsed runtime. On average, there should be little difference in the cost. +- Mode solves that are part of an FDTD simulation (i.e. for mode sources and monitors) are now charged at the same flex credit cost as a corresponding standalone mode solver call. +- Any `FreqMonitor.freqs` or `Source.source_time.freq0` smaller than `1e5` now raise an error as this must be incorrect setup that is outside the Tidy3D intended range (note default frequency is `Hz`). +- When using complex fields (e.g. with Bloch boundaries), FluxTimeMonitor and frequency-domain fields (including derived quantities like flux) now only use the real part of the time-domain electric field. +- Indent for the json string of Tidy3D models has been changed to `None` when used internally; kept as `indent=4` for writing to `json` and `yaml` files. +- API for specifying one or more nonlinear models via `NonlinearSpec.models`. +- `freqs` and `direction` are optional in `ModeSolver` methods converting to monitor and source, respectively. If not supplied, uses the values from the `ModeSolver` instance calling the method. +- Removed spurious ``-1`` factor in field amplitudes injected by field sources in some cases. The injected ``E``-field should now exactly match the analytic, mode, or custom fields that the source is expected to inject, both in the forward and in the backward direction. +- Restriction on the maximum memory that a monitor would need internally during the solver run, even if the final monitor data is smaller. +- Restriction on the maximum size of mode solver data produced by a `ModeSolver` server call. +- Updated versions of `boto3`, `requests`, and `click`. +- python 3.7 no longer tested nor supported. +- Removed warning that monitors now have `colocate=True` by default. +- If `PML` or any absorbing boundary condition is used along a direction where the `Simulation` size is zero, an error will be raised, rather than just a warning. +- Remove warning that monitors now have `colocate=True` by default. +- Internal refactor of Web API functionality. +- `Geometry.from_gds` doesn't create unnecessary groups of single elements. + +### Fixed +- Fixed energy leakage in TFSF when using complex fields. +- Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. +- If input to circular filters in adjoint have size smaller than the diameter, instead of erroring, warn user and truncate the filter kernel accordingly. +- When writing the json string of a model to an `hdf5` file, the string is split into chunks if it has more than a set (very large) number of characters. This fixes potential error if the string size is more than 4GB. +- Proper equality checking between `Tidy3dBaseModel` instances, which takes `DataArray` values and coords into account and handles `np.ndarray` types. +- Correctly set the contour length scale when exporting 2D (or 1D) structures with custom medium to GDSII. +- Improved error handling if file can not be downloaded from server. +- Fix for detection of file extensions for file names with dots. +- Restrict to `matplotlib` >= 3.5, avoiding bug in plotting `CustomMedium`. +- Fixes `ComponentModeler` batch file being different in different sessions by use of deterministic hash function for computing batch filename. +- Can pass `kwargs` to `ComponentModeler.plot_sim()` to use in `Simulation.plot()`. +- Ensure that mode solver fields are returned in single precision if `ModeSolver.ModeSpec.precision == "single"`. +- If there are no adjoint sources for a simulation involved in an objective function, make a mock source with zero amplitude and warn user. + ## [2.4.3] - 2023-10-16 ### Added @@ -58,7 +123,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Bug in adjoint plugin when `JaxBox` is less than 1 grid cell thick. - Bug in `adjoint` plugin where `JaxSimulation.structures` did not accept structures containing `td.PolySlab`. - ## [2.4.0] - 2023-9-11 ### Added @@ -71,7 +135,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Specifically, non-dispersive and dispersive mediums with heat and/or charge perturbation models can be defined through classes `PerturbationMedium` and `PerturbationPoleResidue`, where perturbations to each parameter is specified using class `ParameterPerturbation`. A convenience function `Simulation.perturbed_mediums_copy` is added to class `Simulation` which applies heat and/or charge fields to mediums containing perturbation models. -- Added `hlim` and `vlim` kwargs to `Simulation.plot()` and `Simulation.plot_eps()` for setting horizontal and veritcal plot limits. +- Added `hlim` and `vlim` kwargs to `Simulation.plot()` and `Simulation.plot_eps()` for setting horizontal and vertical plot limits. - Added support for chi3 nonlinearity via `NonlinearSusceptibility` class. - Spatial downsampling allowed in ``PermittivityMonitor`` through the ``interval_space`` argument. - `ClipOperation` geometry type allows the construction of complex geometries through boolean operations. @@ -268,7 +332,7 @@ that the fields match exactly except for a ``pi`` phase shift. This interpretati - `JaxCustomMedium` accepts a maximum of 250,000 grid cells to avoid slow server-side processing. - `PolySlab.inside` now uses `matplotlib.path.contains_points`. - `JaxCustomMedium` accepts a maximum of 250,000 grid cells. -- Logging messages are supressed and summarized to avoid repetitions. +- Logging messages are suppressed and summarized to avoid repetitions. ### Fixed - Log messages provide the correct caller origin (file name and line number). @@ -367,7 +431,7 @@ that the fields match exactly except for a ``pi`` phase shift. This interpretati - Validate `slab_bounds` for `PolySlab`. ### Changed -- Tidy3D account authentication done solely through API key. Migration option offered for useres with old username / password authentication. +- Tidy3D account authentication done solely through API key. Migration option offered for users with old username / password authentication. - `export_matlib_to_file` in `material_library` exports material's full name in addition to abbreviation. - Simpler progress bars for `run_async`. - Medium property `n_cfl` added to adjust time step size according to CFL condition. @@ -509,7 +573,7 @@ method for computing the overlap integral over two sets of frequency-domain fiel ### Changed - Minimum flex unit charge reduced from `0.1` to `0.025`. -- Default courant factor was changed from `0.9` to `0.99`. +- Default Courant factor was changed from `0.9` to `0.99`. - A point dipole source placed on a symmetry plane now always has twice the amplitude of the same source in a simulation without the symmetry plane, as expected by continuity with the case when the dipole is slightly off the symmetry plane, in which case there are effectively two dipoles, the original one and its mirror image. Previously, the amplitude was only doubled for dipoles polarized normal @@ -597,7 +661,7 @@ which fields are to be projected is now determined automatically based on the me ### Added - New classes of near-to-far monitors for server-side computation of the near field to far field projection. -- Option to exlude `DataArray` Fields from a `Tidy3dBaseModel` json. +- Option to exclude `DataArray` Fields from a `Tidy3dBaseModel` json. - Option to save/load all models to/from `hdf5` format. - Option to load base models without validation. - Support negative sidewall angle for slanted `PolySlab`-s. @@ -607,7 +671,7 @@ which fields are to be projected is now determined automatically based on the me ### Fixed - Raise a more meaningful error if login failed after `MAX_ATTEMPTS`. - Environment login credentials set to `""` are now ignored and credentials stored to file are still looked for. -- Improved subpixel coefficients computation around sharp edges, cornes, and three-structure intersections. +- Improved subpixel coefficients computation around sharp edges, corners, and three-structure intersections. ### Changed - Major refactor of the way data structures are used internally. @@ -649,7 +713,7 @@ which fields are to be projected is now determined automatically based on the me ## [1.4.1] - 2022-6-13 ### Fixed -- Bug in plotting polarization of a nomral incidence source for some `angle_phi`. +- Bug in plotting polarization of a normal incidence source for some `angle_phi`. - Bloch vector values required to be real rather than complex. - Web security mitigation. @@ -664,9 +728,9 @@ which fields are to be projected is now determined automatically based on the me ### Added - Bloch periodic boundary conditions, enabling modeling of angled plane wave. -- `GeometryGroup` object to associate several `Geometry` intances in a single `Structure` leading to improved performance for many objects. +- `GeometryGroup` object to associate several `Geometry` instances in a single `Structure` leading to improved performance for many objects. - Ability to uniquely specify boundary conditions on all 6 `Simulation` boundaries. -- Options in field montitors for spatial downsampling and evaluation at yee grid centers. +- Options in field monitors for spatial downsampling and evaluation at Yee grid centers. - `BatchData.load()` can load the data for a batch directly from a directory. - Utility for updating `Simulation` objects from old versions of `Tidy3d` to current version. - Explicit `web.` functions for downloading only `simulation.json` and `tidy3d.log` files. @@ -676,7 +740,7 @@ which fields are to be projected is now determined automatically based on the me - Uses `shapely` instead of `gdspy` to merge polygons from a gds cell. - `ComponentModeler` (S matrix tool) stores the `Batch` rather than the `BatchData`. - Custom caching of properties to speed up subsequent calculations. -- Tidy3d configuration now done through setting attributes of `tidy3d.config` object. +- Tidy3D configuration now done through setting attributes of `tidy3d.config` object. ## [1.3.3] - 2022-5-18 @@ -711,7 +775,7 @@ which fields are to be projected is now determined automatically based on the me ### Changed - - The `copy()` method of Tidy3d components is deep by default. + - The `copy()` method of Tidy3D components is deep by default. - Maximum allowed number of distinct materials is now 65530. ### Fixed @@ -771,7 +835,7 @@ which fields are to be projected is now determined automatically based on the me - `webapi` functions now only authenticate when needed. - Credentials storing folder only created when needed. -- Added maximum number of attemtps in authentication. +- Added maximum number of attempts in authentication. - Made plotly plotting faster. - Cached Simulation.medium and Simulation.medium_map computation. @@ -975,7 +1039,8 @@ which fields are to be projected is now determined automatically based on the me - Job and Batch classes for better simulation handling (eventually to fully replace webapi functions). - A large number of small improvements and bug fixes. -[Unreleased]: https://github.com/flexcompute/tidy3d/compare/v2.4.3...develop +[Unreleased]: https://github.com/flexcompute/tidy3d/compare/v2.5.0...develop +[2.5.0]: https://github.com/flexcompute/tidy3d/compare/v2.4.3...v2.5.0 [2.4.3]: https://github.com/flexcompute/tidy3d/compare/v2.4.2...v2.4.3 [2.4.2]: https://github.com/flexcompute/tidy3d/compare/v2.4.1...v2.4.2 [2.4.1]: https://github.com/flexcompute/tidy3d/compare/v2.4.0...v2.4.1 diff --git a/MANIFEST.in b/MANIFEST.in index 694b35980..0d45d9528 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,4 +8,5 @@ include requirements/gdspy.txt include requirements/gdstk.txt include requirements/dev.txt include requirements/trimesh.txt +include requirements/vtk.txt include tidy3d/web/cacert.pem diff --git a/requirements/basic.txt b/requirements/basic.txt index 6b7947e30..ab1eb38e7 100644 --- a/requirements/basic.txt +++ b/requirements/basic.txt @@ -6,7 +6,7 @@ importlib-metadata>=6.0.0 h5netcdf==1.0.2 h5py>=3.0.0 rich<12.6.0 # note: rich >= 12.6 adds double progressbars -matplotlib +matplotlib>=3.5 shapely>=2.0 pydantic==2.* PyYAML diff --git a/requirements/dev.txt b/requirements/dev.txt index b81dcc897..8f124aeef 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -5,6 +5,7 @@ -r gdspy.txt -r jax.txt -r trimesh.txt +-r vtk.txt # required for development pre-commit diff --git a/requirements/vtk.txt b/requirements/vtk.txt new file mode 100644 index 000000000..a4cde2118 --- /dev/null +++ b/requirements/vtk.txt @@ -0,0 +1,4 @@ +# vtk + +vtk<=9.2.6 + diff --git a/requirements/web.txt b/requirements/web.txt index f0a0e3e9c..51abe55ae 100644 --- a/requirements/web.txt +++ b/requirements/web.txt @@ -1,7 +1,7 @@ # to use the web interface (needed to run tasks) -boto3==1.23.1 -requests +boto3==1.28.* +requests==2.31.* pyjwt -click==8.0.3 +click==8.1.* responses diff --git a/setup.py b/setup.py index c0ed27d21..4c59504a9 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ def read_requirements(req_file: str): gdstk_required = read_requirements("requirements/gdstk.txt") gdspy_required = read_requirements("requirements/gdspy.txt") trimesh_required = read_requirements("requirements/trimesh.txt") +vtk_required = read_requirements("requirements/vtk.txt") core_required = read_requirements("requirements/core.txt") core_required += basic_required + web_required dev_required = read_requirements("requirements/dev.txt") @@ -64,7 +65,7 @@ def create_config_folder(): ], packages=setuptools.find_packages(), include_package_data=True, - python_requires=">=3.7", + python_requires=">=3.8", install_requires=core_required, extras_require={ "dev": dev_required, @@ -72,6 +73,7 @@ def create_config_folder(): "gdspy": gdspy_required, "gdstk": gdstk_required, "trimesh": trimesh_required, + "vtk": vtk_required, }, entry_points={ "console_scripts": [ diff --git a/test_local.sh b/test_local.sh index 02ae2ece4..e33222c96 100755 --- a/test_local.sh +++ b/test_local.sh @@ -6,4 +6,7 @@ ruff check tidy3d pytest -rA tests/ +# test no vtk available (must be done separately from other tests to reload tidy3d from scratch) +pytest -rA tests/test_data/_test_datasets_no_vtk.py + pytest --doctest-modules tidy3d/components diff --git a/tests/sims/simulation_1_10_0rc1.json b/tests/sims/simulation_1_10_0rc1.json deleted file mode 100644 index 37664cc96..000000000 --- a/tests/sims/simulation_1_10_0rc1.json +++ /dev/null @@ -1,1354 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 8.0, - 8.0, - 8.0 - ], - "run_time": 1e-12, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - "Infinity", - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - } - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [ - [ - 1.03961212, - 0.00600069867 - ], - [ - 0.231792344, - 0.0200179144 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 2.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - } - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": "PEC", - "frequency_range": null, - "type": "PECMedium" - } - }, - { - "geometry": { - "type": "Cylinder", - "axis": 1, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - -1.0 - ], - "length": 2.0 - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "AnisotropicMedium", - "xx": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "yy": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } - } - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [ - [ - { - "real": 0.0, - "imag": 6206417594288582.0 - }, - { - "real": -0.0, - "imag": -3.311074436985222e+16 - } - ] - ] - } - }, - { - "geometry": { - "type": "TriangleMesh", - "mesh_dataset": { - "type": "TriangleMeshDataset", - "surface_mesh": "TriangleMeshDataArray" - } - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 5.0, - "conductivity": 0.0 - } - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "type": "PointDipole", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 2.0, - 0.0, - 2.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "-", - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - }, - "mode_index": 0 - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.1 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "AstigmaticGaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_sizes": [ - 1.0, - 2.0 - ], - "waist_distances": [ - 3.0, - 4.0 - ] - }, - { - "type": "CustomFieldSource", - "center": [ - 0.0, - 1.0, - 2.0 - ], - "size": [ - 2.0, - 2.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "field_dataset": { - "type": "FieldDataset", - "Ex": "ScalarFieldDataArray", - "Ey": null, - "Ez": null, - "Hx": null, - "Hy": null, - "Hz": null - } - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "PML", - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "minus": { - "name": null, - "type": "Absorber", - "num_layers": 100, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "minus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "Periodic" - }, - "minus": { - "name": null, - "type": "Periodic" - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "fields": [ - "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field_time", - "start": 0.0, - "stop": null, - "interval": 100, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux_time", - "start": 0.0, - "stop": null, - "interval": 1, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "PermittivityMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.1 - ], - "name": "eps", - "freqs": [ - 100000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - } - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "ModeSolverMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode_solver", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "FieldProjectionCartesianMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_cartesian", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 5.0, - "x": [ - -1.0, - 0.0, - 1.0 - ], - "y": [ - -2.0, - -1.0, - 0.0, - 1.0, - 2.0 - ] - }, - { - "type": "FieldProjectionKSpaceMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_kspace", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 1000000.0, - "ux": [ - 0.1, - 0.2 - ], - "uy": [ - 0.3, - 0.4, - 0.5 - ] - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle_exact", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "DiffractionMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "name": "diffraction", - "freqs": [ - 100000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+" - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "dl_min": 0.0, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "CustomGrid", - "dl": [ - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04 - ] - }, - "grid_z": { - "type": "UniformGrid", - "dl": 0.05 - }, - "wavelength": null, - "override_structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - } - ], - "type": "GridSpec" - }, - "shutoff": 0.0001, - "subpixel": false, - "normalize_index": 0, - "courant": 0.8, - "version": "1.10.0rc1" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_3_3.json b/tests/sims/simulation_1_3_3.json deleted file mode 100644 index 6011486e9..000000000 --- a/tests/sims/simulation_1_3_3.json +++ /dev/null @@ -1,337 +0,0 @@ -{ - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Simulation", - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 4e-10, - "grid_size": null, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "center": [ - -1.0, - 0.0, - 0.0 - ], - "type": "Box", - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Box", - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 1.0, - 0.0, - 1.0 - ], - "type": "Sphere", - "radius": 1.4 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 6.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 1.0, - 0.0, - -1.0 - ], - "type": "Cylinder", - "axis": 1, - "length": 2.0, - "radius": 1.4 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 5.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - -0.8333333333333334, - -1.1666666666666667, - 0.0 - ], - "type": "PolySlab", - "axis": 2, - "length": 2.0, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "center": [ - 0.0, - 0.5, - 0.0 - ], - "type": "UniformCurrentSource", - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "center": [ - 0.0, - 0.5, - 0.0 - ], - "type": "PointDipole", - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - } - ], - "monitors": [ - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FluxMonitor", - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "plane", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ] - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FieldMonitor", - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "point", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ] - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "pml_layers": [ - { - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - }, - "type": "PML" - }, - { - "num_layers": 30, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - }, - "type": "PML" - }, - { - "num_layers": 0, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - }, - "type": "PML" - } - ], - "shutoff": 1e-06, - "subpixel": false, - "courant": 0.8, - "version": "1.3.3" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_4_0.json b/tests/sims/simulation_1_4_0.json deleted file mode 100644 index ece017297..000000000 --- a/tests/sims/simulation_1_4_0.json +++ /dev/null @@ -1,575 +0,0 @@ -{ - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Simulation", - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 1e-12, - "grid_size": null, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - -1, - 1 - ], - "structures": [ - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Box", - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Sphere", - "radius": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Cylinder", - "axis": 2, - "length": 1.0, - "radius": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 2.0, - 2.0, - 0.0 - ], - "type": "PolySlab", - "axis": 2, - "length": 2.0, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Sphere", - "radius": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": "t2", - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "center": [ - 2.0, - 2.0, - 0.0 - ], - "geometries": [ - { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "length": 2.0, - "center": [ - 2.0, - 2.0, - 0.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - } - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 1.0, - "coeffs": [ - [ - 1.0, - 1.0 - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "UniformCurrentSource", - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "center": [ - 0.0, - 0.0, - -4.0 - ], - "type": "PlaneWave", - "size": [ - "Infinity", - "Infinity", - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 2.0 - }, - { - "center": [ - 0.0, - 0.0, - -4.0 - ], - "type": "GaussianBeam", - "size": [ - 1.0, - 1.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.0, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "ModeSource", - "size": [ - 0.0, - 1.0, - 1.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "sort_by": "largest_neff", - "angle_theta": 0.0, - "angle_phi": 0.0, - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "Absorber", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "minus": { - "name": null, - "type": "PML", - "num_layers": 12, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "PMCBoundary" - }, - "minus": { - "name": null, - "type": "PMCBoundary" - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "PECBoundary" - }, - "minus": { - "name": null, - "type": "StablePML", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.0, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 5.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.9 - } - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FieldMonitor", - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "field", - "freqs": [ - 1, - 2, - 3 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FieldTimeMonitor", - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "fieldtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FluxMonitor", - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "flux", - "freqs": [ - 1, - 2, - 3 - ] - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FluxTimeMonitor", - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "fluxtime", - "start": 1e-12, - "stop": null, - "interval": 3 - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "ModeMonitor", - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode", - "freqs": [ - 1, - 2 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "sort_by": "largest_neff", - "angle_theta": 0.0, - "angle_phi": 0.0, - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "shutoff": 1e-05, - "subpixel": true, - "courant": 0.9, - "version": "1.4.0" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_4_1.json b/tests/sims/simulation_1_4_1.json deleted file mode 100644 index 64ddde67d..000000000 --- a/tests/sims/simulation_1_4_1.json +++ /dev/null @@ -1,575 +0,0 @@ -{ - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Simulation", - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 1e-12, - "grid_size": null, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - -1, - 1 - ], - "structures": [ - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Box", - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Sphere", - "radius": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Cylinder", - "axis": 2, - "length": 1.0, - "radius": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 2.0, - 2.0, - 0.0 - ], - "type": "PolySlab", - "axis": 2, - "length": 2.0, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "Sphere", - "radius": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": "t2", - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "center": [ - 2.0, - 2.0, - 0.0 - ], - "geometries": [ - { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "length": 2.0, - "center": [ - 2.0, - 2.0, - 0.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - } - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 1.0, - "coeffs": [ - [ - 1.0, - 1.0 - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "UniformCurrentSource", - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "center": [ - 0.0, - 0.0, - -4.0 - ], - "type": "PlaneWave", - "size": [ - "Infinity", - "Infinity", - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 2.0 - }, - { - "center": [ - 0.0, - 0.0, - -4.0 - ], - "type": "GaussianBeam", - "size": [ - 1.0, - 1.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.0, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "ModeSource", - "size": [ - 0.0, - 1.0, - 1.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "sort_by": "largest_neff", - "angle_theta": 0.0, - "angle_phi": 0.0, - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "Absorber", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "minus": { - "name": null, - "type": "PML", - "num_layers": 12, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "PMCBoundary" - }, - "minus": { - "name": null, - "type": "PMCBoundary" - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "PECBoundary" - }, - "minus": { - "name": null, - "type": "StablePML", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.0, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 5.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.9 - } - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FieldMonitor", - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "field", - "freqs": [ - 1, - 2, - 3 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FieldTimeMonitor", - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "fieldtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FluxMonitor", - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "flux", - "freqs": [ - 1, - 2, - 3 - ] - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "FluxTimeMonitor", - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "fluxtime", - "start": 1e-12, - "stop": null, - "interval": 3 - }, - { - "center": [ - 0.0, - 0.0, - 0.0 - ], - "type": "ModeMonitor", - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode", - "freqs": [ - 1, - 2 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "sort_by": "largest_neff", - "angle_theta": 0.0, - "angle_phi": 0.0, - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "shutoff": 1e-05, - "subpixel": true, - "courant": 0.9, - "version": "1.4.1" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_5_0.json b/tests/sims/simulation_1_5_0.json deleted file mode 100644 index 65e8b03d4..000000000 --- a/tests/sims/simulation_1_5_0.json +++ /dev/null @@ -1,593 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 1e-12, - "grid_size": null, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - -1, - 1 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": "t2", - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - } - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 1.0, - "coeffs": [ - [ - 1.0, - 1.0 - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - "Infinity", - "Infinity", - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 2.0 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.0, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 1.0, - 1.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "Absorber", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "minus": { - "name": null, - "type": "PML", - "num_layers": 12, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "PMCBoundary" - }, - "minus": { - "name": null, - "type": "PMCBoundary" - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "PECBoundary" - }, - "minus": { - "name": null, - "type": "StablePML", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.0, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 5.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.9 - } - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "field", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "fieldtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "flux", - "freqs": [ - 1.0, - 2.0, - 3.0 - ] - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "fluxtime", - "start": 1e-12, - "stop": null, - "interval": 3 - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode", - "freqs": [ - 1.0, - 2.0 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - }, - { - "type": "ModeFieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode_field", - "freqs": [ - 1.0, - 2.0 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "shutoff": 1e-05, - "subpixel": true, - "courant": 0.9, - "version": "1.5.0" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_6_0.json b/tests/sims/simulation_1_6_0.json deleted file mode 100644 index 81fce8428..000000000 --- a/tests/sims/simulation_1_6_0.json +++ /dev/null @@ -1,564 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 1e-12, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - -1, - 1 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": "t2", - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - } - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 1.0, - "coeffs": [ - [ - 1.0, - 1.0 - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - "Infinity", - "Infinity", - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 2.0 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.0, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 1.0, - 1.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "Absorber", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "minus": { - "name": null, - "type": "PML", - "num_layers": 12, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "PMCBoundary" - }, - "minus": { - "name": null, - "type": "PMCBoundary" - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "PECBoundary" - }, - "minus": { - "name": null, - "type": "StablePML", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.0, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 5.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.9 - } - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "field", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "fieldtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "flux", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "fluxtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode", - "freqs": [ - 1.0, - 2.0 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "shutoff": 1e-05, - "subpixel": true, - "normalize_index": 0, - "courant": 0.9, - "version": "1.6.0" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_6_1.json b/tests/sims/simulation_1_6_1.json deleted file mode 100644 index 93c883d79..000000000 --- a/tests/sims/simulation_1_6_1.json +++ /dev/null @@ -1,564 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 1e-12, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - -1, - 1 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": "t2", - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - } - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 1.0, - "coeffs": [ - [ - 1.0, - 1.0 - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - "Infinity", - "Infinity", - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 2.0 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.0, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 1.0, - 1.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "Absorber", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "minus": { - "name": null, - "type": "PML", - "num_layers": 12, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "PMCBoundary" - }, - "minus": { - "name": null, - "type": "PMCBoundary" - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "PECBoundary" - }, - "minus": { - "name": null, - "type": "StablePML", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.0, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 5.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.9 - } - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "field", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "fieldtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "flux", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "fluxtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode", - "freqs": [ - 1.0, - 2.0 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "shutoff": 1e-05, - "subpixel": true, - "normalize_index": 0, - "courant": 0.9, - "version": "1.6.1" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_6_2.json b/tests/sims/simulation_1_6_2.json deleted file mode 100644 index 354083f40..000000000 --- a/tests/sims/simulation_1_6_2.json +++ /dev/null @@ -1,565 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 1e-12, - "grid_size": null, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - -1, - 1 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": "t2", - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - } - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 1.0, - "coeffs": [ - [ - 1.0, - 1.0 - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - "Infinity", - "Infinity", - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 2.0 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.0, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 1.0, - 1.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "Absorber", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "minus": { - "name": null, - "type": "PML", - "num_layers": 12, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "PMCBoundary" - }, - "minus": { - "name": null, - "type": "PMCBoundary" - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "PECBoundary" - }, - "minus": { - "name": null, - "type": "StablePML", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.0, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 5.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.9 - } - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "field", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "fieldtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "flux", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "fluxtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode", - "freqs": [ - 1.0, - 2.0 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "shutoff": 1e-05, - "subpixel": true, - "normalize_index": 0, - "courant": 0.9, - "version": "1.6.2" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_6_3.json b/tests/sims/simulation_1_6_3.json deleted file mode 100644 index 9cf9475be..000000000 --- a/tests/sims/simulation_1_6_3.json +++ /dev/null @@ -1,565 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 1e-12, - "grid_size": null, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - -1, - 1 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 1.0, - "coeffs": [] - }, - "name": "t2", - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - 0.0, - 0.0 - ], - [ - 2.0, - 3.0 - ], - [ - 4.0, - 3.0 - ] - ] - }, - { - "type": "Cylinder", - "axis": 2, - "radius": 1.0, - "center": [ - 0.0, - 0.0, - 0.0 - ], - "length": 1.0 - } - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 1.0, - "coeffs": [ - [ - 1.0, - 1.0 - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - "Infinity", - "Infinity", - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 2.0 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - -4.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.0, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 1.0, - 1.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 1.0, - "fwidth": 1.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "Absorber", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "minus": { - "name": null, - "type": "PML", - "num_layers": 12, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "PMCBoundary" - }, - "minus": { - "name": null, - "type": "PMCBoundary" - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "PECBoundary" - }, - "minus": { - "name": null, - "type": "StablePML", - "num_layers": 40, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.0, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 5.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.9 - } - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "field", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "fieldtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "flux", - "freqs": [ - 1.0, - 2.0, - 3.0 - ], - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "fluxtime", - "start": 1e-12, - "stop": null, - "interval": 3, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 0.0, - 1.0 - ], - "name": "mode", - "freqs": [ - 1.0, - 2.0 - ], - "mode_spec": { - "num_modes": 3, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_z": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "wavelength": null, - "override_structures": [], - "type": "GridSpec" - }, - "shutoff": 1e-05, - "subpixel": true, - "normalize_index": 0, - "courant": 0.9, - "version": "1.6.3" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_7_0.json b/tests/sims/simulation_1_7_0.json deleted file mode 100644 index 3e426d516..000000000 --- a/tests/sims/simulation_1_7_0.json +++ /dev/null @@ -1,1101 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 4e-10, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - "Infinity", - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [ - [ - 1.03961212, - 0.00600069867 - ], - [ - 0.231792344, - 0.0200179144 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 2.0, - 3.0 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - } - ] - }, - "medium": { - "name": "PEC", - "frequency_range": null, - "type": "PECMedium" - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Cylinder", - "axis": 1, - "radius": 1.0, - "center": [ - 1.0, - 0.0, - -1.0 - ], - "length": 2.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "AnisotropicMedium", - "xx": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "yy": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [ - [ - { - "real": 0.0, - "imag": 6206417594288582.0 - }, - { - "real": -0.0, - "imag": -3.311074436985222e+16 - } - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "type": "PointDipole", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - "Infinity", - 0.0, - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 0 - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.1 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "AstigmaticGaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_sizes": [ - 1.0, - 2.0 - ], - "waist_distances": [ - 3.0, - 4.0 - ] - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "PML", - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "minus": { - "name": null, - "type": "Absorber", - "num_layers": 100, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": { - "real": 1.0, - "imag": 0.0 - } - }, - "minus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": { - "real": 1.0, - "imag": 0.0 - } - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "Periodic" - }, - "minus": { - "name": null, - "type": "Periodic" - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "fields": [ - "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field_time", - "start": 0.0, - "stop": null, - "interval": 1, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux_time", - "start": 0.0, - "stop": null, - "interval": 1, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "PermittivityMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "eps", - "freqs": [ - 10000000000000.0 - ] - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - }, - { - "type": "ModeSolverMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode_solver", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - }, - { - "type": "Near2FarAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "n2f_angle", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "normal_dir": null, - "exclude_surfaces": null, - "fields": [ - "Ntheta", - "Nphi", - "Ltheta", - "Lphi" - ], - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "Near2FarCartesianMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "n2f_cartesian", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "normal_dir": null, - "exclude_surfaces": null, - "fields": [ - "Ntheta", - "Nphi", - "Ltheta", - "Lphi" - ], - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "plane_axis": 2, - "plane_distance": 5.0, - "x": [ - -1.0, - 0.0, - 1.0 - ], - "y": [ - -2.0, - -1.0, - 0.0, - 1.0, - 2.0 - ] - }, - { - "type": "DiffractionMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "name": "diffraction", - "freqs": [ - 100000000000000.0, - 200000000000000.0 - ], - "normal_dir": "+", - "orders_x": [ - 0 - ], - "orders_y": [ - 0 - ] - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "CustomGrid", - "dl": [ - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01 - ] - }, - "grid_z": { - "type": "UniformGrid", - "dl": 0.01 - }, - "wavelength": null, - "override_structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - } - ], - "type": "GridSpec" - }, - "shutoff": 1e-06, - "subpixel": false, - "normalize_index": 0, - "courant": 0.8, - "version": "1.7.0" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_7_1.json b/tests/sims/simulation_1_7_1.json deleted file mode 100644 index 4850210a9..000000000 --- a/tests/sims/simulation_1_7_1.json +++ /dev/null @@ -1,1108 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 10.0, - 10.0, - 10.0 - ], - "run_time": 4e-10, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - "Infinity", - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [ - [ - 1.03961212, - 0.00600069867 - ], - [ - 0.231792344, - 0.0200179144 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 2.0, - 3.0 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - } - ] - }, - "medium": { - "name": "PEC", - "frequency_range": null, - "type": "PECMedium" - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "Cylinder", - "axis": 1, - "radius": 1.0, - "center": [ - 1.0, - 0.0, - -1.0 - ], - "length": 2.0 - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "AnisotropicMedium", - "xx": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "yy": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } - }, - "name": null, - "type": "Structure" - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "sidewall_angle": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [ - [ - { - "real": 0.0, - "imag": 6206417594288582.0 - }, - { - "real": -0.0, - "imag": -3.311074436985222e+16 - } - ] - ] - }, - "name": null, - "type": "Structure" - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "type": "PointDipole", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - "Infinity", - 0.0, - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "-", - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - }, - "mode_index": 0 - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.1 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "AstigmaticGaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_sizes": [ - 1.0, - 2.0 - ], - "waist_distances": [ - 3.0, - 4.0 - ] - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "PML", - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "minus": { - "name": null, - "type": "Absorber", - "num_layers": 100, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": { - "real": 1.0, - "imag": 0.0 - } - }, - "minus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": { - "real": 1.0, - "imag": 0.0 - } - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "Periodic" - }, - "minus": { - "name": null, - "type": "Periodic" - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "fields": [ - "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field_time", - "start": 0.0, - "stop": null, - "interval": 1, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux_time", - "start": 0.0, - "stop": null, - "interval": 1, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "PermittivityMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ], - "name": "eps", - "freqs": [ - 10000000000000.0 - ] - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - }, - { - "type": "ModeSolverMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode_solver", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "type": "ModeSpec" - } - }, - { - "type": "Near2FarAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "n2f_angle", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "normal_dir": null, - "exclude_surfaces": null, - "fields": [ - "Ntheta", - "Nphi", - "Ltheta", - "Lphi" - ], - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "Near2FarCartesianMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "n2f_cartesian", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "normal_dir": null, - "exclude_surfaces": null, - "fields": [ - "Ntheta", - "Nphi", - "Ltheta", - "Lphi" - ], - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "plane_axis": 2, - "plane_distance": 5.0, - "x": [ - -1.0, - 0.0, - 1.0 - ], - "y": [ - -2.0, - -1.0, - 0.0, - 1.0, - 2.0 - ] - }, - { - "type": "DiffractionMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "name": "diffraction", - "freqs": [ - 100000000000000.0, - 200000000000000.0 - ], - "normal_dir": "+", - "orders_x": [ - 0 - ], - "orders_y": [ - 0 - ], - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - } - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "CustomGrid", - "dl": [ - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01, - 0.01 - ] - }, - "grid_z": { - "type": "UniformGrid", - "dl": 0.01 - }, - "wavelength": null, - "override_structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "name": null, - "type": "Structure" - } - ], - "type": "GridSpec" - }, - "shutoff": 1e-06, - "subpixel": false, - "normalize_index": 0, - "courant": 0.8, - "version": "1.7.1" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_8_0.json b/tests/sims/simulation_1_8_0.json deleted file mode 100644 index 70d1b2cc4..000000000 --- a/tests/sims/simulation_1_8_0.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "Simulation", "center": [0.0, 0.0, 0.0], "size": [10.0, 10.0, 10.0], "run_time": 4e-10, "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "symmetry": [0, 0, 0], "structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, "Infinity", 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0}}, {"geometry": {"type": "Sphere", "radius": 1.0, "center": [1.0, 0.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Sellmeier", "coeffs": [[1.03961212, 0.00600069867], [0.231792344, 0.0200179144]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [[1.0, 2.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "GeometryGroup", "geometries": [{"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}]}, "name": null, "type": "Structure", "medium": {"name": "PEC", "frequency_range": null, "type": "PECMedium"}}, {"geometry": {"type": "Cylinder", "axis": 1, "sidewall_angle": 0.0, "reference_plane": "bottom", "radius": 1.0, "center": [1.0, 0.0, -1.0], "length": 2.0}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "AnisotropicMedium", "xx": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "yy": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}, "zz": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 3.0, "conductivity": 0.0}}}, {"geometry": {"type": "PolySlab", "axis": 2, "sidewall_angle": 0.0, "reference_plane": "bottom", "slab_bounds": [-1.0, 1.0], "dilation": 0.0, "vertices": [[-1.5, -1.5], [-0.5, -1.5], [-0.5, -0.5]]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [[{"real": 0.0, "imag": 6206417594288582.0}, {"real": -0.0, "imag": -3.311074436985222e+16}]]}}], "sources": [{"type": "UniformCurrentSource", "center": [0.0, 0.5, 0.0], "size": [0.0, 0.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Hx"}, {"type": "PointDipole", "center": [0.0, 0.5, 0.0], "size": [0, 0, 0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Ex"}, {"type": "ModeSource", "center": [0.0, 0.5, 0.0], "size": ["Infinity", 0.0, "Infinity"], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "-", "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}, "mode_index": 0}, {"type": "PlaneWave", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 0.1}, {"type": "GaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_radius": 1.0, "waist_distance": 0.0}, {"type": "AstigmaticGaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_sizes": [1.0, 2.0], "waist_distances": [3.0, 4.0]}, {"type": "CustomFieldSource", "center": [1.0, 2.0, 3.0], "size": [2.0, 2.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "field_dataset": {"type": "FieldDataset", "Ex": "ScalarFieldDataArray", "Ey": null, "Ez": null, "Hx": null, "Hy": null, "Hz": null}}], "boundary_spec": {"x": {"plus": {"name": null, "type": "PML", "num_layers": 20, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 1.5, "type": "PMLParams", "kappa_order": 3, "kappa_min": 1.0, "kappa_max": 3.0, "alpha_order": 1, "alpha_min": 0.0, "alpha_max": 0.0}}, "minus": {"name": null, "type": "Absorber", "num_layers": 100, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 6.4, "type": "AbsorberParams"}}, "type": "Boundary"}, "y": {"plus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "minus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "type": "Boundary"}, "z": {"plus": {"name": null, "type": "Periodic"}, "minus": {"name": null, "type": "Periodic"}, "type": "Boundary"}, "type": "BoundarySpec"}, "monitors": [{"type": "FieldMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field", "freqs": [150000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "fields": ["Ex"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FieldTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field_time", "start": 0.0, "stop": null, "interval": 1, "fields": ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FluxMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null}, {"type": "FluxTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux_time", "start": 0.0, "stop": null, "interval": 1, "normal_dir": "+", "exclude_surfaces": null}, {"type": "PermittivityMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0], "name": "eps", "freqs": [10000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}}, {"type": "ModeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "ModeSolverMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode_solver", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "FieldProjectionCartesianMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_cartesian", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 5.0, "x": [-1.0, 0.0, 1.0], "y": [-2.0, -1.0, 0.0, 1.0, 2.0]}, {"type": "FieldProjectionKSpaceMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_kspace", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 1000000.0, "ux": [0.1, 0.2], "uy": [0.3, 0.4, 0.5]}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle_exact", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "DiffractionMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "name": "diffraction", "freqs": [100000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+"}], "grid_spec": {"grid_x": {"type": "AutoGrid", "min_steps_per_wvl": 10.0, "max_scale": 1.4, "dl_min": 0.0, "mesher": {"type": "GradedMesher"}}, "grid_y": {"type": "CustomGrid", "dl": [0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]}, "grid_z": {"type": "UniformGrid", "dl": 0.01}, "wavelength": null, "override_structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}], "type": "GridSpec"}, "shutoff": 1e-06, "subpixel": false, "normalize_index": 0, "courant": 0.8, "version": "1.8.0"} diff --git a/tests/sims/simulation_1_8_1.json b/tests/sims/simulation_1_8_1.json deleted file mode 100644 index c4ec680c1..000000000 --- a/tests/sims/simulation_1_8_1.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "Simulation", "center": [0.0, 0.0, 0.0], "size": [8.0, 8.0, 8.0], "run_time": 1e-12, "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "symmetry": [0, 0, 0], "structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, "Infinity", 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0}}, {"geometry": {"type": "Sphere", "radius": 1.0, "center": [1.0, 0.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Sellmeier", "coeffs": [[1.03961212, 0.00600069867], [0.231792344, 0.0200179144]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [[1.0, 2.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "GeometryGroup", "geometries": [{"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}]}, "name": null, "type": "Structure", "medium": {"name": "PEC", "frequency_range": null, "type": "PECMedium"}}, {"geometry": {"type": "Cylinder", "axis": 1, "sidewall_angle": 0.0, "reference_plane": "bottom", "radius": 1.0, "center": [1.0, 0.0, -1.0], "length": 2.0}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "AnisotropicMedium", "xx": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "yy": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}, "zz": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 3.0, "conductivity": 0.0}}}, {"geometry": {"type": "PolySlab", "axis": 2, "sidewall_angle": 0.0, "reference_plane": "bottom", "slab_bounds": [-1.0, 1.0], "dilation": 0.0, "vertices": [[-1.5, -1.5], [-0.5, -1.5], [-0.5, -0.5]]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [[{"real": 0.0, "imag": 6206417594288582.0}, {"real": -0.0, "imag": -3.311074436985222e+16}]]}}], "sources": [{"type": "UniformCurrentSource", "center": [0.0, 0.5, 0.0], "size": [0.0, 0.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Hx"}, {"type": "PointDipole", "center": [0.0, 0.5, 0.0], "size": [0, 0, 0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Ex"}, {"type": "ModeSource", "center": [0.0, 0.5, 0.0], "size": [2.0, 0.0, 2.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "-", "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}, "mode_index": 0}, {"type": "PlaneWave", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 0.1}, {"type": "GaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_radius": 1.0, "waist_distance": 0.0}, {"type": "AstigmaticGaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_sizes": [1.0, 2.0], "waist_distances": [3.0, 4.0]}, {"type": "CustomFieldSource", "center": [0.0, 1.0, 2.0], "size": [2.0, 2.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "field_dataset": {"type": "FieldDataset", "Ex": "ScalarFieldDataArray", "Ey": null, "Ez": null, "Hx": null, "Hy": null, "Hz": null}}], "boundary_spec": {"x": {"plus": {"name": null, "type": "PML", "num_layers": 20, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 1.5, "type": "PMLParams", "kappa_order": 3, "kappa_min": 1.0, "kappa_max": 3.0, "alpha_order": 1, "alpha_min": 0.0, "alpha_max": 0.0}}, "minus": {"name": null, "type": "Absorber", "num_layers": 100, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 6.4, "type": "AbsorberParams"}}, "type": "Boundary"}, "y": {"plus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "minus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "type": "Boundary"}, "z": {"plus": {"name": null, "type": "Periodic"}, "minus": {"name": null, "type": "Periodic"}, "type": "Boundary"}, "type": "BoundarySpec"}, "monitors": [{"type": "FieldMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field", "freqs": [150000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "fields": ["Ex"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FieldTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field_time", "start": 0.0, "stop": null, "interval": 100, "fields": ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FluxMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null}, {"type": "FluxTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux_time", "start": 0.0, "stop": null, "interval": 1, "normal_dir": "+", "exclude_surfaces": null}, {"type": "PermittivityMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.1], "name": "eps", "freqs": [100000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}}, {"type": "ModeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "ModeSolverMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode_solver", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "FieldProjectionCartesianMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_cartesian", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 5.0, "x": [-1.0, 0.0, 1.0], "y": [-2.0, -1.0, 0.0, 1.0, 2.0]}, {"type": "FieldProjectionKSpaceMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_kspace", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 1000000.0, "ux": [0.1, 0.2], "uy": [0.3, 0.4, 0.5]}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle_exact", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "DiffractionMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "name": "diffraction", "freqs": [100000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+"}], "grid_spec": {"grid_x": {"type": "AutoGrid", "min_steps_per_wvl": 10.0, "max_scale": 1.4, "dl_min": 0.0, "mesher": {"type": "GradedMesher"}}, "grid_y": {"type": "CustomGrid", "dl": [0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04]}, "grid_z": {"type": "UniformGrid", "dl": 0.05}, "wavelength": null, "override_structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}], "type": "GridSpec"}, "shutoff": 0.0001, "subpixel": false, "normalize_index": 0, "courant": 0.8, "version": "1.8.1"} \ No newline at end of file diff --git a/tests/sims/simulation_1_8_2.json b/tests/sims/simulation_1_8_2.json deleted file mode 100644 index b450e3f2a..000000000 --- a/tests/sims/simulation_1_8_2.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "Simulation", "center": [0.0, 0.0, 0.0], "size": [8.0, 8.0, 8.0], "run_time": 1e-12, "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "symmetry": [0, 0, 0], "structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, "Infinity", 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0}}, {"geometry": {"type": "Sphere", "radius": 1.0, "center": [1.0, 0.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Sellmeier", "coeffs": [[1.03961212, 0.00600069867], [0.231792344, 0.0200179144]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [[1.0, 2.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "GeometryGroup", "geometries": [{"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}]}, "name": null, "type": "Structure", "medium": {"name": "PEC", "frequency_range": null, "type": "PECMedium"}}, {"geometry": {"type": "Cylinder", "axis": 1, "sidewall_angle": 0.0, "reference_plane": "bottom", "radius": 1.0, "center": [1.0, 0.0, -1.0], "length": 2.0}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "AnisotropicMedium", "xx": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "yy": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}, "zz": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 3.0, "conductivity": 0.0}}}, {"geometry": {"type": "PolySlab", "axis": 2, "sidewall_angle": 0.0, "reference_plane": "bottom", "slab_bounds": [-1.0, 1.0], "dilation": 0.0, "vertices": [[-1.5, -1.5], [-0.5, -1.5], [-0.5, -0.5]]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [[{"real": 0.0, "imag": 6206417594288582.0}, {"real": -0.0, "imag": -3.311074436985222e+16}]]}}], "sources": [{"type": "UniformCurrentSource", "center": [0.0, 0.5, 0.0], "size": [0.0, 0.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Hx"}, {"type": "PointDipole", "center": [0.0, 0.5, 0.0], "size": [0, 0, 0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Ex"}, {"type": "ModeSource", "center": [0.0, 0.5, 0.0], "size": [2.0, 0.0, 2.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "-", "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}, "mode_index": 0}, {"type": "PlaneWave", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 0.1}, {"type": "GaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_radius": 1.0, "waist_distance": 0.0}, {"type": "AstigmaticGaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_sizes": [1.0, 2.0], "waist_distances": [3.0, 4.0]}, {"type": "CustomFieldSource", "center": [0.0, 1.0, 2.0], "size": [2.0, 2.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "field_dataset": {"type": "FieldDataset", "Ex": "ScalarFieldDataArray", "Ey": null, "Ez": null, "Hx": null, "Hy": null, "Hz": null}}], "boundary_spec": {"x": {"plus": {"name": null, "type": "PML", "num_layers": 20, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 1.5, "type": "PMLParams", "kappa_order": 3, "kappa_min": 1.0, "kappa_max": 3.0, "alpha_order": 1, "alpha_min": 0.0, "alpha_max": 0.0}}, "minus": {"name": null, "type": "Absorber", "num_layers": 100, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 6.4, "type": "AbsorberParams"}}, "type": "Boundary"}, "y": {"plus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "minus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "type": "Boundary"}, "z": {"plus": {"name": null, "type": "Periodic"}, "minus": {"name": null, "type": "Periodic"}, "type": "Boundary"}, "type": "BoundarySpec"}, "monitors": [{"type": "FieldMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field", "freqs": [150000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "fields": ["Ex"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FieldTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field_time", "start": 0.0, "stop": null, "interval": 100, "fields": ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FluxMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null}, {"type": "FluxTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux_time", "start": 0.0, "stop": null, "interval": 1, "normal_dir": "+", "exclude_surfaces": null}, {"type": "PermittivityMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.1], "name": "eps", "freqs": [100000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}}, {"type": "ModeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "ModeSolverMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode_solver", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "FieldProjectionCartesianMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_cartesian", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 5.0, "x": [-1.0, 0.0, 1.0], "y": [-2.0, -1.0, 0.0, 1.0, 2.0]}, {"type": "FieldProjectionKSpaceMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_kspace", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 1000000.0, "ux": [0.1, 0.2], "uy": [0.3, 0.4, 0.5]}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle_exact", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "DiffractionMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "name": "diffraction", "freqs": [100000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+"}], "grid_spec": {"grid_x": {"type": "AutoGrid", "min_steps_per_wvl": 10.0, "max_scale": 1.4, "dl_min": 0.0, "mesher": {"type": "GradedMesher"}}, "grid_y": {"type": "CustomGrid", "dl": [0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04]}, "grid_z": {"type": "UniformGrid", "dl": 0.05}, "wavelength": null, "override_structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}], "type": "GridSpec"}, "shutoff": 0.0001, "subpixel": false, "normalize_index": 0, "courant": 0.8, "version": "1.8.2"} \ No newline at end of file diff --git a/tests/sims/simulation_1_8_3.json b/tests/sims/simulation_1_8_3.json deleted file mode 100644 index 500a17b28..000000000 --- a/tests/sims/simulation_1_8_3.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "Simulation", "center": [0.0, 0.0, 0.0], "size": [8.0, 8.0, 8.0], "run_time": 1e-12, "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "symmetry": [0, 0, 0], "structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, "Infinity", 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0}}, {"geometry": {"type": "Sphere", "radius": 1.0, "center": [1.0, 0.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Sellmeier", "coeffs": [[1.03961212, 0.00600069867], [0.231792344, 0.0200179144]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [[1.0, 2.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "GeometryGroup", "geometries": [{"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}]}, "name": null, "type": "Structure", "medium": {"name": "PEC", "frequency_range": null, "type": "PECMedium"}}, {"geometry": {"type": "Cylinder", "axis": 1, "sidewall_angle": 0.0, "reference_plane": "bottom", "radius": 1.0, "center": [1.0, 0.0, -1.0], "length": 2.0}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "AnisotropicMedium", "xx": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "yy": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}, "zz": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 3.0, "conductivity": 0.0}}}, {"geometry": {"type": "PolySlab", "axis": 2, "sidewall_angle": 0.0, "reference_plane": "bottom", "slab_bounds": [-1.0, 1.0], "dilation": 0.0, "vertices": [[-1.5, -1.5], [-0.5, -1.5], [-0.5, -0.5]]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [[{"real": 0.0, "imag": 6206417594288582.0}, {"real": -0.0, "imag": -3.311074436985222e+16}]]}}], "sources": [{"type": "UniformCurrentSource", "center": [0.0, 0.5, 0.0], "size": [0.0, 0.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Hx"}, {"type": "PointDipole", "center": [0.0, 0.5, 0.0], "size": [0, 0, 0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Ex"}, {"type": "ModeSource", "center": [0.0, 0.5, 0.0], "size": [2.0, 0.0, 2.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "-", "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}, "mode_index": 0}, {"type": "PlaneWave", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 0.1}, {"type": "GaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_radius": 1.0, "waist_distance": 0.0}, {"type": "AstigmaticGaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_sizes": [1.0, 2.0], "waist_distances": [3.0, 4.0]}, {"type": "CustomFieldSource", "center": [0.0, 1.0, 2.0], "size": [2.0, 2.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "field_dataset": {"type": "FieldDataset", "Ex": "XR.DATAARRAY", "Ey": null, "Ez": null, "Hx": null, "Hy": null, "Hz": null}}], "boundary_spec": {"x": {"plus": {"name": null, "type": "PML", "num_layers": 20, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 1.5, "type": "PMLParams", "kappa_order": 3, "kappa_min": 1.0, "kappa_max": 3.0, "alpha_order": 1, "alpha_min": 0.0, "alpha_max": 0.0}}, "minus": {"name": null, "type": "Absorber", "num_layers": 100, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 6.4, "type": "AbsorberParams"}}, "type": "Boundary"}, "y": {"plus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "minus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "type": "Boundary"}, "z": {"plus": {"name": null, "type": "Periodic"}, "minus": {"name": null, "type": "Periodic"}, "type": "Boundary"}, "type": "BoundarySpec"}, "monitors": [{"type": "FieldMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field", "freqs": [150000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "fields": ["Ex"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FieldTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field_time", "start": 0.0, "stop": null, "interval": 100, "fields": ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FluxMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null}, {"type": "FluxTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux_time", "start": 0.0, "stop": null, "interval": 1, "normal_dir": "+", "exclude_surfaces": null}, {"type": "PermittivityMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.1], "name": "eps", "freqs": [100000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}}, {"type": "ModeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "ModeSolverMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode_solver", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "FieldProjectionCartesianMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_cartesian", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 5.0, "x": [-1.0, 0.0, 1.0], "y": [-2.0, -1.0, 0.0, 1.0, 2.0]}, {"type": "FieldProjectionKSpaceMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_kspace", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 1000000.0, "ux": [0.1, 0.2], "uy": [0.3, 0.4, 0.5]}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle_exact", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "DiffractionMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "name": "diffraction", "freqs": [100000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+"}], "grid_spec": {"grid_x": {"type": "AutoGrid", "min_steps_per_wvl": 10.0, "max_scale": 1.4, "dl_min": 0.0, "mesher": {"type": "GradedMesher"}}, "grid_y": {"type": "CustomGrid", "dl": [0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04]}, "grid_z": {"type": "UniformGrid", "dl": 0.05}, "wavelength": null, "override_structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}], "type": "GridSpec"}, "shutoff": 0.0001, "subpixel": false, "normalize_index": 0, "courant": 0.8, "version": "1.8.3"} \ No newline at end of file diff --git a/tests/sims/simulation_1_8_4.json b/tests/sims/simulation_1_8_4.json deleted file mode 100644 index 3ae92ceb3..000000000 --- a/tests/sims/simulation_1_8_4.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "Simulation", "center": [0.0, 0.0, 0.0], "size": [8.0, 8.0, 8.0], "run_time": 1e-12, "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "symmetry": [0, 0, 0], "structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, "Infinity", 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0}}, {"geometry": {"type": "Sphere", "radius": 1.0, "center": [1.0, 0.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Sellmeier", "coeffs": [[1.03961212, 0.00600069867], [0.231792344, 0.0200179144]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [[1.0, 2.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "GeometryGroup", "geometries": [{"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}]}, "name": null, "type": "Structure", "medium": {"name": "PEC", "frequency_range": null, "type": "PECMedium"}}, {"geometry": {"type": "Cylinder", "axis": 1, "sidewall_angle": 0.0, "reference_plane": "bottom", "radius": 1.0, "center": [1.0, 0.0, -1.0], "length": 2.0}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "AnisotropicMedium", "xx": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "yy": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}, "zz": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 3.0, "conductivity": 0.0}}}, {"geometry": {"type": "PolySlab", "axis": 2, "sidewall_angle": 0.0, "reference_plane": "bottom", "slab_bounds": [-1.0, 1.0], "dilation": 0.0, "vertices": [[-1.5, -1.5], [-0.5, -1.5], [-0.5, -0.5]]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [[{"real": 0.0, "imag": 6206417594288582.0}, {"real": -0.0, "imag": -3.311074436985222e+16}]]}}], "sources": [{"type": "UniformCurrentSource", "center": [0.0, 0.5, 0.0], "size": [0.0, 0.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Hx"}, {"type": "PointDipole", "center": [0.0, 0.5, 0.0], "size": [0, 0, 0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Ex"}, {"type": "ModeSource", "center": [0.0, 0.5, 0.0], "size": [2.0, 0.0, 2.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "-", "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}, "mode_index": 0}, {"type": "PlaneWave", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 0.1}, {"type": "GaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_radius": 1.0, "waist_distance": 0.0}, {"type": "AstigmaticGaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_sizes": [1.0, 2.0], "waist_distances": [3.0, 4.0]}, {"type": "CustomFieldSource", "center": [0.0, 1.0, 2.0], "size": [2.0, 2.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "field_dataset": {"type": "FieldDataset", "Ex": "XR.DATAARRAY", "Ey": null, "Ez": null, "Hx": null, "Hy": null, "Hz": null}}], "boundary_spec": {"x": {"plus": {"name": null, "type": "PML", "num_layers": 20, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 1.5, "type": "PMLParams", "kappa_order": 3, "kappa_min": 1.0, "kappa_max": 3.0, "alpha_order": 1, "alpha_min": 0.0, "alpha_max": 0.0}}, "minus": {"name": null, "type": "Absorber", "num_layers": 100, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 6.4, "type": "AbsorberParams"}}, "type": "Boundary"}, "y": {"plus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "minus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "type": "Boundary"}, "z": {"plus": {"name": null, "type": "Periodic"}, "minus": {"name": null, "type": "Periodic"}, "type": "Boundary"}, "type": "BoundarySpec"}, "monitors": [{"type": "FieldMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field", "freqs": [150000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "fields": ["Ex"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FieldTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field_time", "start": 0.0, "stop": null, "interval": 100, "fields": ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FluxMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null}, {"type": "FluxTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux_time", "start": 0.0, "stop": null, "interval": 1, "normal_dir": "+", "exclude_surfaces": null}, {"type": "PermittivityMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.1], "name": "eps", "freqs": [100000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}}, {"type": "ModeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "ModeSolverMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode_solver", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "FieldProjectionCartesianMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_cartesian", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 5.0, "x": [-1.0, 0.0, 1.0], "y": [-2.0, -1.0, 0.0, 1.0, 2.0]}, {"type": "FieldProjectionKSpaceMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_kspace", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 1000000.0, "ux": [0.1, 0.2], "uy": [0.3, 0.4, 0.5]}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle_exact", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "DiffractionMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "name": "diffraction", "freqs": [100000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+"}], "grid_spec": {"grid_x": {"type": "AutoGrid", "min_steps_per_wvl": 10.0, "max_scale": 1.4, "dl_min": 0.0, "mesher": {"type": "GradedMesher"}}, "grid_y": {"type": "CustomGrid", "dl": [0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04]}, "grid_z": {"type": "UniformGrid", "dl": 0.05}, "wavelength": null, "override_structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}], "type": "GridSpec"}, "shutoff": 0.0001, "subpixel": false, "normalize_index": 0, "courant": 0.8, "version": "1.8.4"} \ No newline at end of file diff --git a/tests/sims/simulation_1_9_0.json b/tests/sims/simulation_1_9_0.json deleted file mode 100644 index e21708d73..000000000 --- a/tests/sims/simulation_1_9_0.json +++ /dev/null @@ -1,1364 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 8.0, - 8.0, - 8.0 - ], - "run_time": 1e-12, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - "Infinity", - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - } - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [ - [ - 1.03961212, - 0.00600069867 - ], - [ - 0.231792344, - 0.0200179144 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 2.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - } - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": "PEC", - "frequency_range": null, - "type": "PECMedium" - } - }, - { - "geometry": { - "type": "Cylinder", - "axis": 1, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - -1.0 - ], - "length": 2.0 - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "AnisotropicMedium", - "xx": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "yy": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } - } - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [ - [ - { - "real": 0.0, - "imag": 6206417594288582.0 - }, - { - "real": -0.0, - "imag": -3.311074436985222e+16 - } - ] - ] - } - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "type": "PointDipole", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 2.0, - 0.0, - 2.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "-", - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - }, - "mode_index": 0 - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.1 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "AstigmaticGaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_sizes": [ - 1.0, - 2.0 - ], - "waist_distances": [ - 3.0, - 4.0 - ] - }, - { - "type": "CustomFieldSource", - "center": [ - 0.0, - 1.0, - 2.0 - ], - "size": [ - 2.0, - 2.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "field_dataset": { - "type": "FieldDataset", - "Ex": "ScalarFieldDataArray", - "Ey": null, - "Ez": null, - "Hx": null, - "Hy": null, - "Hz": null - } - }, - { - "type": "TFSF", - "center": [ - 1.0, - 2.0, - -3.0 - ], - "size": [ - 2.5, - 2.5, - 0.5 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.5235987755982988, - "angle_phi": 0.6283185307179586, - "pol_angle": 0.0, - "injection_axis": 2 - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "PML", - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "minus": { - "name": null, - "type": "Absorber", - "num_layers": 100, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "minus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "Periodic" - }, - "minus": { - "name": null, - "type": "Periodic" - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "fields": [ - "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field_time", - "start": 0.0, - "stop": null, - "interval": 100, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux_time", - "start": 0.0, - "stop": null, - "interval": 1, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "PermittivityMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.1 - ], - "name": "eps", - "freqs": [ - 100000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - } - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "ModeSolverMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode_solver", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "FieldProjectionCartesianMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_cartesian", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 5.0, - "x": [ - -1.0, - 0.0, - 1.0 - ], - "y": [ - -2.0, - -1.0, - 0.0, - 1.0, - 2.0 - ] - }, - { - "type": "FieldProjectionKSpaceMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_kspace", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 1000000.0, - "ux": [ - 0.1, - 0.2 - ], - "uy": [ - 0.3, - 0.4, - 0.5 - ] - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle_exact", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "DiffractionMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "name": "diffraction", - "freqs": [ - 100000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+" - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "dl_min": 0.0, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "CustomGrid", - "dl": [ - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04 - ], - "custom_offset": null - }, - "grid_z": { - "type": "UniformGrid", - "dl": 0.05 - }, - "wavelength": null, - "override_structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - } - ], - "type": "GridSpec" - }, - "shutoff": 0.0001, - "subpixel": false, - "normalize_index": 0, - "courant": 0.8, - "version": "1.9.0" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_9_0rc1.json b/tests/sims/simulation_1_9_0rc1.json deleted file mode 100644 index 640ca435e..000000000 --- a/tests/sims/simulation_1_9_0rc1.json +++ /dev/null @@ -1 +0,0 @@ -{"type": "Simulation", "center": [0.0, 0.0, 0.0], "size": [8.0, 8.0, 8.0], "run_time": 1e-12, "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "symmetry": [0, 0, 0], "structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, "Infinity", 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0}}, {"geometry": {"type": "Sphere", "radius": 1.0, "center": [1.0, 0.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Sellmeier", "coeffs": [[1.03961212, 0.00600069867], [0.231792344, 0.0200179144]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [[1.0, 2.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [[1.0, 3.0]]}}, {"geometry": {"type": "GeometryGroup", "geometries": [{"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}]}, "name": null, "type": "Structure", "medium": {"name": "PEC", "frequency_range": null, "type": "PECMedium"}}, {"geometry": {"type": "Cylinder", "axis": 1, "sidewall_angle": 0.0, "reference_plane": "bottom", "radius": 1.0, "center": [1.0, 0.0, -1.0], "length": 2.0}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "AnisotropicMedium", "xx": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0}, "yy": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}, "zz": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 3.0, "conductivity": 0.0}}}, {"geometry": {"type": "PolySlab", "axis": 2, "sidewall_angle": 0.0, "reference_plane": "bottom", "slab_bounds": [-1.0, 1.0], "dilation": 0.0, "vertices": [[-1.5, -1.5], [-0.5, -1.5], [-0.5, -0.5]]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [[{"real": 0.0, "imag": 6206417594288582.0}, {"real": -0.0, "imag": -3.311074436985222e+16}]]}}], "sources": [{"type": "UniformCurrentSource", "center": [0.0, 0.5, 0.0], "size": [0.0, 0.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Hx"}, {"type": "PointDipole", "center": [0.0, 0.5, 0.0], "size": [0, 0, 0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "polarization": "Ex"}, {"type": "ModeSource", "center": [0.0, 0.5, 0.0], "size": [2.0, 0.0, 2.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "-", "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}, "mode_index": 0}, {"type": "PlaneWave", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 0.1}, {"type": "GaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_radius": 1.0, "waist_distance": 0.0}, {"type": "AstigmaticGaussianBeam", "center": [0.0, 0.0, 0.0], "size": [0.0, 3.0, 3.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 1.5707963267948966, "waist_sizes": [1.0, 2.0], "waist_distances": [3.0, 4.0]}, {"type": "CustomFieldSource", "center": [0.0, 1.0, 2.0], "size": [2.0, 2.0, 0.0], "source_time": {"amplitude": 1.0, "phase": 0.0, "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, "offset": 5.0}, "name": null, "field_dataset": {"type": "FieldDataset", "Ex": "ScalarFieldDataArray", "Ey": null, "Ez": null, "Hx": null, "Hy": null, "Hz": null}}], "boundary_spec": {"x": {"plus": {"name": null, "type": "PML", "num_layers": 20, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 1.5, "type": "PMLParams", "kappa_order": 3, "kappa_min": 1.0, "kappa_max": 3.0, "alpha_order": 1, "alpha_min": 0.0, "alpha_max": 0.0}}, "minus": {"name": null, "type": "Absorber", "num_layers": 100, "parameters": {"sigma_order": 3, "sigma_min": 0.0, "sigma_max": 6.4, "type": "AbsorberParams"}}, "type": "Boundary"}, "y": {"plus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "minus": {"name": null, "type": "BlochBoundary", "bloch_vec": 1.0}, "type": "Boundary"}, "z": {"plus": {"name": null, "type": "Periodic"}, "minus": {"name": null, "type": "Periodic"}, "type": "Boundary"}, "type": "BoundarySpec"}, "monitors": [{"type": "FieldMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field", "freqs": [150000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "fields": ["Ex"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FieldTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 0.0, 0.0], "name": "field_time", "start": 0.0, "stop": null, "interval": 100, "fields": ["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"], "interval_space": [1, 1, 1], "colocate": false}, {"type": "FluxMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null}, {"type": "FluxTimeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "flux_time", "start": 0.0, "stop": null, "interval": 1, "normal_dir": "+", "exclude_surfaces": null}, {"type": "PermittivityMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.1], "name": "eps", "freqs": [100000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}}, {"type": "ModeMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "ModeSolverMonitor", "center": [0.0, 0.0, 0.0], "size": [1.0, 1.0, 0.0], "name": "mode_solver", "freqs": [200000000000000.0, 250000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "mode_spec": {"num_modes": 1, "target_neff": null, "num_pml": [0, 0], "filter_pol": null, "angle_theta": 0.0, "angle_phi": 0.0, "precision": "single", "bend_radius": null, "bend_axis": null, "track_freq": "central", "type": "ModeSpec"}}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "FieldProjectionCartesianMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_cartesian", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 5.0, "x": [-1.0, 0.0, 1.0], "y": [-2.0, -1.0, 0.0, 1.0, 2.0]}, {"type": "FieldProjectionKSpaceMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_kspace", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_axis": 2, "proj_distance": 1000000.0, "ux": [0.1, 0.2], "uy": [0.3, 0.4, 0.5]}, {"type": "FieldProjectionAngleMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, 2.0, 2.0], "name": "proj_angle_exact", "freqs": [250000000000000.0, 300000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+", "exclude_surfaces": null, "custom_origin": [1.0, 2.0, 3.0], "far_field_approx": true, "proj_distance": 1000000.0, "theta": [-1.5707963267948966, -1.5390630676677268, -1.5073298085405573, -1.4755965494133876, -1.443863290286218, -1.4121300311590483, -1.3803967720318788, -1.348663512904709, -1.3169302537775396, -1.2851969946503699, -1.2534637355232003, -1.2217304763960306, -1.189997217268861, -1.1582639581416914, -1.1265306990145216, -1.0947974398873521, -1.0630641807601826, -1.0313309216330129, -0.9995976625058433, -0.9678644033786736, -0.936131144251504, -0.9043978851243344, -0.8726646259971648, -0.8409313668699951, -0.8091981077428254, -0.7774648486156558, -0.7457315894884862, -0.7139983303613165, -0.6822650712341469, -0.6505318121069773, -0.6187985529798077, -0.5870652938526381, -0.5553320347254684, -0.5235987755982987, -0.4918655164711292, -0.46013225734395946, -0.42839899821678995, -0.3966657390896202, -0.3649324799624507, -0.333199220835281, -0.30146596170811146, -0.26973270258094173, -0.23799944345377222, -0.2062661843266025, -0.17453292519943298, -0.14279966607226324, -0.11106640694509373, -0.079333147817924, -0.047599888690754266, -0.015866629563584755, 0.015866629563584977, 0.04759988869075449, 0.07933314781792422, 0.11106640694509373, 0.14279966607226346, 0.17453292519943298, 0.2062661843266027, 0.23799944345377222, 0.26973270258094195, 0.30146596170811146, 0.3331992208352812, 0.3649324799624507, 0.39666573908962044, 0.42839899821678995, 0.4601322573439597, 0.4918655164711292, 0.5235987755982991, 0.5553320347254687, 0.5870652938526382, 0.6187985529798077, 0.6505318121069776, 0.6822650712341471, 0.7139983303613167, 0.7457315894884862, 0.7774648486156561, 0.8091981077428256, 0.8409313668699951, 0.8726646259971647, 0.9043978851243346, 0.9361311442515041, 0.9678644033786736, 0.9995976625058436, 1.031330921633013, 1.0630641807601826, 1.0947974398873521, 1.126530699014522, 1.1582639581416916, 1.189997217268861, 1.2217304763960306, 1.2534637355232006, 1.28519699465037, 1.3169302537775396, 1.348663512904709, 1.380396772031879, 1.4121300311590486, 1.443863290286218, 1.475596549413388, 1.5073298085405575, 1.539063067667727, 1.5707963267948966], "phi": [0.0, 1.5707963267948966]}, {"type": "DiffractionMonitor", "center": [0.0, 0.0, 0.0], "size": [0.0, "Infinity", "Infinity"], "name": "diffraction", "freqs": [100000000000000.0, 200000000000000.0], "apodization": {"start": null, "end": null, "width": null, "type": "ApodizationSpec"}, "normal_dir": "+"}], "grid_spec": {"grid_x": {"type": "AutoGrid", "min_steps_per_wvl": 10.0, "max_scale": 1.4, "dl_min": 0.0, "mesher": {"type": "GradedMesher"}}, "grid_y": {"type": "CustomGrid", "dl": [0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04, 0.04]}, "grid_z": {"type": "UniformGrid", "dl": 0.05}, "wavelength": null, "override_structures": [{"geometry": {"type": "Box", "center": [-1.0, 0.0, 0.0], "size": [1.0, 1.0, 1.0]}, "name": null, "type": "Structure", "medium": {"name": null, "frequency_range": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0}}], "type": "GridSpec"}, "shutoff": 0.0001, "subpixel": false, "normalize_index": 0, "courant": 0.8, "version": "1.9.0rc1"} \ No newline at end of file diff --git a/tests/sims/simulation_1_9_0rc2.json b/tests/sims/simulation_1_9_0rc2.json deleted file mode 100644 index 32db946ce..000000000 --- a/tests/sims/simulation_1_9_0rc2.json +++ /dev/null @@ -1,1336 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 8.0, - 8.0, - 8.0 - ], - "run_time": 1e-12, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - "Infinity", - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - } - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [ - [ - 1.03961212, - 0.00600069867 - ], - [ - 0.231792344, - 0.0200179144 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 2.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - } - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": "PEC", - "frequency_range": null, - "type": "PECMedium" - } - }, - { - "geometry": { - "type": "Cylinder", - "axis": 1, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - -1.0 - ], - "length": 2.0 - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "AnisotropicMedium", - "xx": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "yy": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } - } - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [ - [ - { - "real": 0.0, - "imag": 6206417594288582.0 - }, - { - "real": -0.0, - "imag": -3.311074436985222e+16 - } - ] - ] - } - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "type": "PointDipole", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 2.0, - 0.0, - 2.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "-", - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - }, - "mode_index": 0 - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.1 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "AstigmaticGaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_sizes": [ - 1.0, - 2.0 - ], - "waist_distances": [ - 3.0, - 4.0 - ] - }, - { - "type": "CustomFieldSource", - "center": [ - 0.0, - 1.0, - 2.0 - ], - "size": [ - 2.0, - 2.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "field_dataset": { - "type": "FieldDataset", - "Ex": "ScalarFieldDataArray", - "Ey": null, - "Ez": null, - "Hx": null, - "Hy": null, - "Hz": null - } - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "PML", - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "minus": { - "name": null, - "type": "Absorber", - "num_layers": 100, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "minus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "Periodic" - }, - "minus": { - "name": null, - "type": "Periodic" - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "fields": [ - "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field_time", - "start": 0.0, - "stop": null, - "interval": 100, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux_time", - "start": 0.0, - "stop": null, - "interval": 1, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "PermittivityMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.1 - ], - "name": "eps", - "freqs": [ - 100000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - } - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "ModeSolverMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode_solver", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "FieldProjectionCartesianMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_cartesian", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 5.0, - "x": [ - -1.0, - 0.0, - 1.0 - ], - "y": [ - -2.0, - -1.0, - 0.0, - 1.0, - 2.0 - ] - }, - { - "type": "FieldProjectionKSpaceMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_kspace", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 1000000.0, - "ux": [ - 0.1, - 0.2 - ], - "uy": [ - 0.3, - 0.4, - 0.5 - ] - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle_exact", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "DiffractionMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "name": "diffraction", - "freqs": [ - 100000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+" - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "dl_min": 0.0, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "CustomGrid", - "dl": [ - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04 - ] - }, - "grid_z": { - "type": "UniformGrid", - "dl": 0.05 - }, - "wavelength": null, - "override_structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - } - ], - "type": "GridSpec" - }, - "shutoff": 0.0001, - "subpixel": false, - "normalize_index": 0, - "courant": 0.8, - "version": "1.9.0rc2" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_9_1.json b/tests/sims/simulation_1_9_1.json deleted file mode 100644 index 0875c195f..000000000 --- a/tests/sims/simulation_1_9_1.json +++ /dev/null @@ -1,1336 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 8.0, - 8.0, - 8.0 - ], - "run_time": 1e-12, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - "Infinity", - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - } - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [ - [ - 1.03961212, - 0.00600069867 - ], - [ - 0.231792344, - 0.0200179144 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 2.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - } - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": "PEC", - "frequency_range": null, - "type": "PECMedium" - } - }, - { - "geometry": { - "type": "Cylinder", - "axis": 1, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - -1.0 - ], - "length": 2.0 - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "AnisotropicMedium", - "xx": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "yy": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } - } - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [ - [ - { - "real": 0.0, - "imag": 6206417594288582.0 - }, - { - "real": -0.0, - "imag": -3.311074436985222e+16 - } - ] - ] - } - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "type": "PointDipole", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 2.0, - 0.0, - 2.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "-", - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - }, - "mode_index": 0 - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.1 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "AstigmaticGaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_sizes": [ - 1.0, - 2.0 - ], - "waist_distances": [ - 3.0, - 4.0 - ] - }, - { - "type": "CustomFieldSource", - "center": [ - 0.0, - 1.0, - 2.0 - ], - "size": [ - 2.0, - 2.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "field_dataset": { - "type": "FieldDataset", - "Ex": "ScalarFieldDataArray", - "Ey": null, - "Ez": null, - "Hx": null, - "Hy": null, - "Hz": null - } - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "PML", - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "minus": { - "name": null, - "type": "Absorber", - "num_layers": 100, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "minus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "Periodic" - }, - "minus": { - "name": null, - "type": "Periodic" - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "fields": [ - "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field_time", - "start": 0.0, - "stop": null, - "interval": 100, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux_time", - "start": 0.0, - "stop": null, - "interval": 1, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "PermittivityMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.1 - ], - "name": "eps", - "freqs": [ - 100000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - } - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "ModeSolverMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode_solver", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "FieldProjectionCartesianMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_cartesian", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 5.0, - "x": [ - -1.0, - 0.0, - 1.0 - ], - "y": [ - -2.0, - -1.0, - 0.0, - 1.0, - 2.0 - ] - }, - { - "type": "FieldProjectionKSpaceMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_kspace", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 1000000.0, - "ux": [ - 0.1, - 0.2 - ], - "uy": [ - 0.3, - 0.4, - 0.5 - ] - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle_exact", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "DiffractionMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "name": "diffraction", - "freqs": [ - 100000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+" - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "dl_min": 0.0, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "CustomGrid", - "dl": [ - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04 - ] - }, - "grid_z": { - "type": "UniformGrid", - "dl": 0.05 - }, - "wavelength": null, - "override_structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - } - ], - "type": "GridSpec" - }, - "shutoff": 0.0001, - "subpixel": false, - "normalize_index": 0, - "courant": 0.8, - "version": "1.9.1" -} \ No newline at end of file diff --git a/tests/sims/simulation_1_9_2.json b/tests/sims/simulation_1_9_2.json deleted file mode 100644 index ee68dd88e..000000000 --- a/tests/sims/simulation_1_9_2.json +++ /dev/null @@ -1,1336 +0,0 @@ -{ - "type": "Simulation", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 8.0, - 8.0, - 8.0 - ], - "run_time": 1e-12, - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "symmetry": [ - 0, - 0, - 0 - ], - "structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - "Infinity", - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 3.0 - } - }, - { - "geometry": { - "type": "Sphere", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Sellmeier", - "coeffs": [ - [ - 1.03961212, - 0.00600069867 - ], - [ - 0.231792344, - 0.0200179144 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Lorentz", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 2.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Debye", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Drude", - "eps_inf": 2.0, - "coeffs": [ - [ - 1.0, - 3.0 - ] - ] - } - }, - { - "geometry": { - "type": "GeometryGroup", - "geometries": [ - { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - } - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": "PEC", - "frequency_range": null, - "type": "PECMedium" - } - }, - { - "geometry": { - "type": "Cylinder", - "axis": 1, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "radius": 1.0, - "center": [ - 1.0, - 0.0, - -1.0 - ], - "length": 2.0 - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "AnisotropicMedium", - "xx": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 1.0, - "conductivity": 0.0 - }, - "yy": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } - } - }, - { - "geometry": { - "type": "PolySlab", - "axis": 2, - "sidewall_angle": 0.0, - "reference_plane": "bottom", - "slab_bounds": [ - -1.0, - 1.0 - ], - "dilation": 0.0, - "vertices": [ - [ - -1.5, - -1.5 - ], - [ - -0.5, - -1.5 - ], - [ - -0.5, - -0.5 - ] - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "PoleResidue", - "eps_inf": 1.0, - "poles": [ - [ - { - "real": 0.0, - "imag": 6206417594288582.0 - }, - { - "real": -0.0, - "imag": -3.311074436985222e+16 - } - ] - ] - } - } - ], - "sources": [ - { - "type": "UniformCurrentSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Hx" - }, - { - "type": "PointDipole", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 0, - 0, - 0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "polarization": "Ex" - }, - { - "type": "ModeSource", - "center": [ - 0.0, - 0.5, - 0.0 - ], - "size": [ - 2.0, - 0.0, - 2.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "-", - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - }, - "mode_index": 0 - }, - { - "type": "PlaneWave", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 0.1 - }, - { - "type": "GaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_radius": 1.0, - "waist_distance": 0.0 - }, - { - "type": "AstigmaticGaussianBeam", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 3.0, - 3.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "num_freqs": 1, - "direction": "+", - "angle_theta": 0.0, - "angle_phi": 0.0, - "pol_angle": 1.5707963267948966, - "waist_sizes": [ - 1.0, - 2.0 - ], - "waist_distances": [ - 3.0, - 4.0 - ] - }, - { - "type": "CustomFieldSource", - "center": [ - 0.0, - 1.0, - 2.0 - ], - "size": [ - 2.0, - 2.0, - 0.0 - ], - "source_time": { - "amplitude": 1.0, - "phase": 0.0, - "type": "GaussianPulse", - "freq0": 200000000000000.0, - "fwidth": 40000000000000.0, - "offset": 5.0 - }, - "name": null, - "field_dataset": { - "type": "FieldDataset", - "Ex": "ScalarFieldDataArray", - "Ey": null, - "Ez": null, - "Hx": null, - "Hy": null, - "Hz": null - } - } - ], - "boundary_spec": { - "x": { - "plus": { - "name": null, - "type": "PML", - "num_layers": 20, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 1.5, - "type": "PMLParams", - "kappa_order": 3, - "kappa_min": 1.0, - "kappa_max": 3.0, - "alpha_order": 1, - "alpha_min": 0.0, - "alpha_max": 0.0 - } - }, - "minus": { - "name": null, - "type": "Absorber", - "num_layers": 100, - "parameters": { - "sigma_order": 3, - "sigma_min": 0.0, - "sigma_max": 6.4, - "type": "AbsorberParams" - } - }, - "type": "Boundary" - }, - "y": { - "plus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "minus": { - "name": null, - "type": "BlochBoundary", - "bloch_vec": 1.0 - }, - "type": "Boundary" - }, - "z": { - "plus": { - "name": null, - "type": "Periodic" - }, - "minus": { - "name": null, - "type": "Periodic" - }, - "type": "Boundary" - }, - "type": "BoundarySpec" - }, - "monitors": [ - { - "type": "FieldMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field", - "freqs": [ - 150000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "fields": [ - "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FieldTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 0.0, - 0.0 - ], - "name": "field_time", - "start": 0.0, - "stop": null, - "interval": 100, - "fields": [ - "Ex", - "Ey", - "Ez", - "Hx", - "Hy", - "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false - }, - { - "type": "FluxMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "FluxTimeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "flux_time", - "start": 0.0, - "stop": null, - "interval": 1, - "normal_dir": "+", - "exclude_surfaces": null - }, - { - "type": "PermittivityMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.1 - ], - "name": "eps", - "freqs": [ - 100000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - } - }, - { - "type": "ModeMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "ModeSolverMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 0.0 - ], - "name": "mode_solver", - "freqs": [ - 200000000000000.0, - 250000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "mode_spec": { - "num_modes": 1, - "target_neff": null, - "num_pml": [ - 0, - 0 - ], - "filter_pol": null, - "angle_theta": 0.0, - "angle_phi": 0.0, - "precision": "single", - "bend_radius": null, - "bend_axis": null, - "track_freq": "central", - "type": "ModeSpec" - } - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "FieldProjectionCartesianMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_cartesian", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 5.0, - "x": [ - -1.0, - 0.0, - 1.0 - ], - "y": [ - -2.0, - -1.0, - 0.0, - 1.0, - 2.0 - ] - }, - { - "type": "FieldProjectionKSpaceMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_kspace", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_axis": 2, - "proj_distance": 1000000.0, - "ux": [ - 0.1, - 0.2 - ], - "uy": [ - 0.3, - 0.4, - 0.5 - ] - }, - { - "type": "FieldProjectionAngleMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - 2.0, - 2.0 - ], - "name": "proj_angle_exact", - "freqs": [ - 250000000000000.0, - 300000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+", - "exclude_surfaces": null, - "custom_origin": [ - 1.0, - 2.0, - 3.0 - ], - "far_field_approx": true, - "proj_distance": 1000000.0, - "theta": [ - -1.5707963267948966, - -1.5390630676677268, - -1.5073298085405573, - -1.4755965494133876, - -1.443863290286218, - -1.4121300311590483, - -1.3803967720318788, - -1.348663512904709, - -1.3169302537775396, - -1.2851969946503699, - -1.2534637355232003, - -1.2217304763960306, - -1.189997217268861, - -1.1582639581416914, - -1.1265306990145216, - -1.0947974398873521, - -1.0630641807601826, - -1.0313309216330129, - -0.9995976625058433, - -0.9678644033786736, - -0.936131144251504, - -0.9043978851243344, - -0.8726646259971648, - -0.8409313668699951, - -0.8091981077428254, - -0.7774648486156558, - -0.7457315894884862, - -0.7139983303613165, - -0.6822650712341469, - -0.6505318121069773, - -0.6187985529798077, - -0.5870652938526381, - -0.5553320347254684, - -0.5235987755982987, - -0.4918655164711292, - -0.46013225734395946, - -0.42839899821678995, - -0.3966657390896202, - -0.3649324799624507, - -0.333199220835281, - -0.30146596170811146, - -0.26973270258094173, - -0.23799944345377222, - -0.2062661843266025, - -0.17453292519943298, - -0.14279966607226324, - -0.11106640694509373, - -0.079333147817924, - -0.047599888690754266, - -0.015866629563584755, - 0.015866629563584977, - 0.04759988869075449, - 0.07933314781792422, - 0.11106640694509373, - 0.14279966607226346, - 0.17453292519943298, - 0.2062661843266027, - 0.23799944345377222, - 0.26973270258094195, - 0.30146596170811146, - 0.3331992208352812, - 0.3649324799624507, - 0.39666573908962044, - 0.42839899821678995, - 0.4601322573439597, - 0.4918655164711292, - 0.5235987755982991, - 0.5553320347254687, - 0.5870652938526382, - 0.6187985529798077, - 0.6505318121069776, - 0.6822650712341471, - 0.7139983303613167, - 0.7457315894884862, - 0.7774648486156561, - 0.8091981077428256, - 0.8409313668699951, - 0.8726646259971647, - 0.9043978851243346, - 0.9361311442515041, - 0.9678644033786736, - 0.9995976625058436, - 1.031330921633013, - 1.0630641807601826, - 1.0947974398873521, - 1.126530699014522, - 1.1582639581416916, - 1.189997217268861, - 1.2217304763960306, - 1.2534637355232006, - 1.28519699465037, - 1.3169302537775396, - 1.348663512904709, - 1.380396772031879, - 1.4121300311590486, - 1.443863290286218, - 1.475596549413388, - 1.5073298085405575, - 1.539063067667727, - 1.5707963267948966 - ], - "phi": [ - 0.0, - 1.5707963267948966 - ] - }, - { - "type": "DiffractionMonitor", - "center": [ - 0.0, - 0.0, - 0.0 - ], - "size": [ - 0.0, - "Infinity", - "Infinity" - ], - "name": "diffraction", - "freqs": [ - 100000000000000.0, - 200000000000000.0 - ], - "apodization": { - "start": null, - "end": null, - "width": null, - "type": "ApodizationSpec" - }, - "normal_dir": "+" - } - ], - "grid_spec": { - "grid_x": { - "type": "AutoGrid", - "min_steps_per_wvl": 10.0, - "max_scale": 1.4, - "dl_min": 0.0, - "mesher": { - "type": "GradedMesher" - } - }, - "grid_y": { - "type": "CustomGrid", - "dl": [ - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04, - 0.04 - ] - }, - "grid_z": { - "type": "UniformGrid", - "dl": 0.05 - }, - "wavelength": null, - "override_structures": [ - { - "geometry": { - "type": "Box", - "center": [ - -1.0, - 0.0, - 0.0 - ], - "size": [ - 1.0, - 1.0, - 1.0 - ] - }, - "name": null, - "type": "Structure", - "medium": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 2.0, - "conductivity": 0.0 - } - } - ], - "type": "GridSpec" - }, - "shutoff": 0.0001, - "subpixel": false, - "normalize_index": 0, - "courant": 0.8, - "version": "1.9.2" -} \ No newline at end of file diff --git a/tests/sims/simulation_2_5_0.h5 b/tests/sims/simulation_2_5_0.h5 new file mode 100644 index 000000000..03ea92ea7 Binary files /dev/null and b/tests/sims/simulation_2_5_0.h5 differ diff --git a/tests/sims/simulation_2_5_0.json b/tests/sims/simulation_2_5_0.json new file mode 100644 index 000000000..b98fa3cf8 --- /dev/null +++ b/tests/sims/simulation_2_5_0.json @@ -0,0 +1,2274 @@ +{ + "type": "Simulation", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 8.0, + 8.0, + 8.0 + ], + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + }, + "structures": [ + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": "dieletric_box", + "type": "Structure", + "medium": { + "name": "dieletric", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 2.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + "Infinity", + 1.0 + ] + }, + "name": "lossy_box", + "type": "Structure", + "medium": { + "name": "lossy_dieletric", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 3.0 + } + }, + { + "geometry": { + "type": "Sphere", + "radius": 1.0, + "center": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": "sellmeier_sphere", + "type": "Structure", + "medium": { + "name": "sellmeier", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Sellmeier", + "coeffs": [ + [ + 1.03961212, + 0.00600069867 + ], + [ + 0.231792344, + 0.0200179144 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": "lorentz_box", + "type": "Structure", + "medium": { + "name": "lorentz", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Lorentz", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 2.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Debye", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Debye", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": "drude_box", + "type": "Structure", + "medium": { + "name": "drude", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Drude", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium2D", + "ss": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + }, + "tt": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + } + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium2D", + "ss": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + }, + "tt": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + } + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": null, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "AnisotropicMedium", + "xx": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + }, + "yy": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + }, + "zz": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + } + }, + { + "geometry": { + "type": "GeometryGroup", + "geometries": [ + { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + } + ] + }, + "name": "pec_group", + "type": "Structure", + "medium": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + } + }, + { + "geometry": { + "type": "Cylinder", + "axis": 1, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "radius": 1.0, + "center": [ + 1.0, + 0.0, + -1.0 + ], + "length": 2.0 + }, + "name": "anisotopic_cylinder", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": null, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "AnisotropicMedium", + "xx": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + }, + "yy": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 2.0, + "conductivity": 0.0 + }, + "zz": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 3.0, + "conductivity": 0.0 + } + } + }, + { + "geometry": { + "type": "PolySlab", + "axis": 2, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "slab_bounds": [ + -1.0, + 1.0 + ], + "dilation": 0.0, + "vertices": [ + [ + -1.5, + -1.5 + ], + [ + -0.5, + -1.5 + ], + [ + -0.5, + -0.5 + ] + ] + }, + "name": "pole_slab", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 6206417594288582.0 + }, + { + "real": -0.0, + "imag": -3.311074436985222e+16 + } + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomMedium", + "interp_method": "nearest", + "subpixel": false, + "eps_dataset": null, + "permittivity": "SpatialDataArray", + "conductivity": null + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomDrude", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomLorentz", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomDebye", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomPoleResidue", + "eps_inf": "SpatialDataArray", + "poles": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomSellmeier", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": { + "type": "NonlinearSusceptibility", + "chi3": 0.1, + "numiters": 20 + }, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": { + "models": [ + { + "type": "NonlinearSusceptibility", + "chi3": 0.1, + "numiters": null + }, + { + "type": "TwoPhotonAbsorption", + "beta": { + "real": 1.0, + "imag": 0.0 + }, + "n0": null + }, + { + "type": "KerrNonlinearity", + "n2": { + "real": 1.0, + "imag": 0.0 + }, + "n0": null + } + ], + "num_iters": 10, + "type": "NonlinearSpec" + }, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "PolySlab", + "axis": 2, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "slab_bounds": [ + -1.0, + 1.0 + ], + "dilation": 0.0, + "vertices": [ + [ + -1.5, + -1.5 + ], + [ + -0.5, + -1.5 + ], + [ + -0.5, + -0.5 + ] + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 6206417594288582.0 + }, + { + "real": -0.0, + "imag": -3.311074436985222e+16 + } + ] + ] + } + }, + { + "geometry": { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + "name": "dieletric_mesh", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 5.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "GeometryGroup", + "geometries": [ + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + } + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 5.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "ClipOperation", + "operation": "symmetric_difference", + "geometry_a": { + "type": "Box", + "center": [ + 0.9, + 0.9, + 0.9 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "geometry_b": { + "type": "Box", + "center": [ + 1.1, + 1.1, + 1.1 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + "name": "clip_operation", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 3.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Transformed", + "geometry": { + "type": "Box", + "center": [ + 1.0, + 1.0, + 1.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "transform": [ + [ + 0.9659258262890683, + -0.25881904510252074, + 0.0, + 0.0 + ], + [ + 0.25881904510252074, + 0.9659258262890683, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "name": "transformed_box", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.5, + "conductivity": 0.0 + } + } + ], + "symmetry": [ + 0, + 0, + 0 + ], + "sources": [ + { + "name": null, + "type": "UniformCurrentSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "interpolate": true, + "polarization": "Hx" + }, + { + "name": null, + "type": "PointDipole", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0, + 0, + 0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "interpolate": true, + "polarization": "Ex" + }, + { + "name": null, + "type": "ModeSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 2.0, + 0.0, + 2.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "num_freqs": 1, + "direction": "-", + "mode_spec": { + "num_modes": 1, + "target_neff": null, + "num_pml": [ + 0, + 0 + ], + "filter_pol": null, + "angle_theta": 0.0, + "angle_phi": 0.0, + "precision": "single", + "bend_radius": null, + "bend_axis": null, + "track_freq": "central", + "group_index_step": false, + "type": "ModeSpec" + }, + "mode_index": 0 + }, + { + "name": null, + "type": "PlaneWave", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + "Infinity", + "Infinity" + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "direction": "+", + "angle_theta": 0.0, + "angle_phi": 0.0, + "pol_angle": 0.1 + }, + { + "name": null, + "type": "GaussianBeam", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 3.0, + 3.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "num_freqs": 1, + "direction": "+", + "angle_theta": 0.0, + "angle_phi": 0.0, + "pol_angle": 1.5707963267948966, + "waist_radius": 1.0, + "waist_distance": 0.0 + }, + { + "name": null, + "type": "AstigmaticGaussianBeam", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 3.0, + 3.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "num_freqs": 1, + "direction": "+", + "angle_theta": 0.0, + "angle_phi": 0.0, + "pol_angle": 1.5707963267948966, + "waist_sizes": [ + 1.0, + 2.0 + ], + "waist_distances": [ + 3.0, + 4.0 + ] + }, + { + "name": null, + "type": "CustomFieldSource", + "center": [ + 0.0, + 1.0, + 2.0 + ], + "size": [ + 2.0, + 2.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "field_dataset": { + "type": "FieldDataset", + "Ex": "ScalarFieldDataArray", + "Ey": null, + "Ez": null, + "Hx": null, + "Hy": null, + "Hz": null + } + }, + { + "name": null, + "type": "CustomCurrentSource", + "center": [ + 0.0, + 1.0, + 2.0 + ], + "size": [ + 2.0, + 2.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "interpolate": true, + "current_dataset": { + "type": "FieldDataset", + "Ex": "ScalarFieldDataArray", + "Ey": null, + "Ez": null, + "Hx": null, + "Hy": null, + "Hz": null + } + }, + { + "name": null, + "type": "TFSF", + "center": [ + 1.0, + 2.0, + -3.0 + ], + "size": [ + 2.5, + 2.5, + 0.5 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "direction": "+", + "angle_theta": 0.5235987755982988, + "angle_phi": 0.6283185307179586, + "pol_angle": 0.0, + "injection_axis": 2 + }, + { + "name": null, + "type": "UniformCurrentSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "CustomSourceTime", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 0.0, + "source_time_dataset": { + "type": "TimeDataset", + "values": "TimeDataArray" + } + }, + "interpolate": true, + "polarization": "Hx" + } + ], + "boundary_spec": { + "x": { + "plus": { + "name": null, + "type": "PML", + "num_layers": 20, + "parameters": { + "sigma_order": 3, + "sigma_min": 0.0, + "sigma_max": 1.5, + "type": "PMLParams", + "kappa_order": 3, + "kappa_min": 1.0, + "kappa_max": 3.0, + "alpha_order": 1, + "alpha_min": 0.0, + "alpha_max": 0.0 + } + }, + "minus": { + "name": null, + "type": "Absorber", + "num_layers": 100, + "parameters": { + "sigma_order": 3, + "sigma_min": 0.0, + "sigma_max": 6.4, + "type": "AbsorberParams" + } + }, + "type": "Boundary" + }, + "y": { + "plus": { + "name": null, + "type": "BlochBoundary", + "bloch_vec": 1.0 + }, + "minus": { + "name": null, + "type": "BlochBoundary", + "bloch_vec": 1.0 + }, + "type": "Boundary" + }, + "z": { + "plus": { + "name": null, + "type": "Periodic" + }, + "minus": { + "name": null, + "type": "Periodic" + }, + "type": "Boundary" + }, + "type": "BoundarySpec" + }, + "monitors": [ + { + "type": "FieldMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "name": "field", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 150000000000000.0, + 200000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "fields": [ + "Ex" + ] + }, + { + "type": "FieldTimeMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "name": "field_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "start": 0.0, + "stop": null, + "interval": 100, + "fields": [ + "Ex", + "Ey", + "Ez", + "Hx", + "Hy", + "Hz" + ] + }, + { + "type": "FluxMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "flux", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 200000000000000.0, + 250000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null + }, + { + "type": "FluxTimeMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "flux_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "start": 0.0, + "stop": null, + "interval": 1, + "normal_dir": "+", + "exclude_surfaces": null + }, + { + "type": "PermittivityMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.1 + ], + "name": "eps", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, + "freqs": [ + 100000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + } + }, + { + "type": "ModeMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "mode", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, + "freqs": [ + 200000000000000.0, + 250000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "mode_spec": { + "num_modes": 1, + "target_neff": null, + "num_pml": [ + 0, + 0 + ], + "filter_pol": null, + "angle_theta": 0.0, + "angle_phi": 0.0, + "precision": "single", + "bend_radius": null, + "bend_axis": null, + "track_freq": "central", + "group_index_step": false, + "type": "ModeSpec" + } + }, + { + "type": "ModeSolverMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "mode_solver", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 200000000000000.0, + 250000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "mode_spec": { + "num_modes": 1, + "target_neff": null, + "num_pml": [ + 0, + 0 + ], + "filter_pol": null, + "angle_theta": 0.0, + "angle_phi": 0.0, + "precision": "single", + "bend_radius": null, + "bend_axis": null, + "track_freq": "central", + "group_index_step": false, + "type": "ModeSpec" + }, + "direction": "+" + }, + { + "type": "FieldProjectionAngleMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_angle", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "medium": null, + "proj_distance": 1000000.0, + "theta": [ + 0.7853981633974483, + 0.8012647929610331, + 0.8171314225246179, + 0.8329980520882028, + 0.8488646816517875, + 0.8647313112153724, + 0.8805979407789571, + 0.896464570342542, + 0.9123311999061268, + 0.9281978294697116, + 0.9440644590332964, + 0.9599310885968813, + 0.975797718160466, + 0.9916643477240509, + 1.0075309772876357, + 1.0233976068512205, + 1.0392642364148053, + 1.05513086597839, + 1.070997495541975, + 1.0868641251055597, + 1.1027307546691445, + 1.1185973842327295, + 1.1344640137963142, + 1.150330643359899, + 1.1661972729234837, + 1.1820639024870687, + 1.1979305320506535, + 1.2137971616142382, + 1.2296637911778232, + 1.245530420741408, + 1.2613970503049927, + 1.2772636798685775, + 1.2931303094321622, + 1.3089969389957472, + 1.324863568559332, + 1.340730198122917, + 1.3565968276865017, + 1.3724634572500864, + 1.3883300868136712, + 1.404196716377256, + 1.4200633459408407, + 1.4359299755044257, + 1.4517966050680104, + 1.4676632346315954, + 1.4835298641951802, + 1.499396493758765, + 1.5152631233223497, + 1.5311297528859344, + 1.5469963824495194, + 1.5628630120131042, + 1.5787296415766892, + 1.594596271140274, + 1.6104629007038587, + 1.6263295302674434, + 1.6421961598310282, + 1.658062789394613, + 1.673929418958198, + 1.6897960485217827, + 1.7056626780853676, + 1.7215293076489524, + 1.7373959372125372, + 1.753262566776122, + 1.7691291963397067, + 1.7849958259032914, + 1.8008624554668764, + 1.8167290850304612, + 1.8325957145940461, + 1.8484623441576309, + 1.8643289737212156, + 1.8801956032848004, + 1.8960622328483854, + 1.9119288624119701, + 1.9277954919755549, + 1.9436621215391396, + 1.9595287511027246, + 1.9753953806663094, + 1.9912620102298941, + 2.007128639793479, + 2.0229952693570636, + 2.038861898920649, + 2.054728528484233, + 2.0705951580478184, + 2.086461787611403, + 2.102328417174988, + 2.1181950467385726, + 2.1340616763021574, + 2.1499283058657426, + 2.165794935429327, + 2.181661564992912, + 2.197528194556497, + 2.2133948241200816, + 2.2292614536836663, + 2.245128083247251, + 2.2609947128108363, + 2.2768613423744206, + 2.292727971938006, + 2.3085946015015906, + 2.3244612310651753, + 2.34032786062876, + 2.356194490192345 + ], + "phi": [ + 0.0, + 0.5235987755982988 + ] + }, + { + "type": "FieldProjectionCartesianMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_cartesian", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "medium": null, + "proj_axis": 2, + "proj_distance": 5.0, + "x": [ + -1.0, + 0.0, + 1.0 + ], + "y": [ + -2.0, + -1.0, + 0.0, + 1.0, + 2.0 + ] + }, + { + "type": "FieldProjectionKSpaceMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_kspace", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "medium": null, + "proj_axis": 2, + "proj_distance": 1000000.0, + "ux": [ + 0.02, + 0.04 + ], + "uy": [ + 0.03, + 0.04, + 0.05 + ] + }, + { + "type": "FieldProjectionAngleMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_angle_exact", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "medium": null, + "proj_distance": 1000000.0, + "theta": [ + 0.7853981633974483, + 0.8012647929610331, + 0.8171314225246179, + 0.8329980520882028, + 0.8488646816517875, + 0.8647313112153724, + 0.8805979407789571, + 0.896464570342542, + 0.9123311999061268, + 0.9281978294697116, + 0.9440644590332964, + 0.9599310885968813, + 0.975797718160466, + 0.9916643477240509, + 1.0075309772876357, + 1.0233976068512205, + 1.0392642364148053, + 1.05513086597839, + 1.070997495541975, + 1.0868641251055597, + 1.1027307546691445, + 1.1185973842327295, + 1.1344640137963142, + 1.150330643359899, + 1.1661972729234837, + 1.1820639024870687, + 1.1979305320506535, + 1.2137971616142382, + 1.2296637911778232, + 1.245530420741408, + 1.2613970503049927, + 1.2772636798685775, + 1.2931303094321622, + 1.3089969389957472, + 1.324863568559332, + 1.340730198122917, + 1.3565968276865017, + 1.3724634572500864, + 1.3883300868136712, + 1.404196716377256, + 1.4200633459408407, + 1.4359299755044257, + 1.4517966050680104, + 1.4676632346315954, + 1.4835298641951802, + 1.499396493758765, + 1.5152631233223497, + 1.5311297528859344, + 1.5469963824495194, + 1.5628630120131042, + 1.5787296415766892, + 1.594596271140274, + 1.6104629007038587, + 1.6263295302674434, + 1.6421961598310282, + 1.658062789394613, + 1.673929418958198, + 1.6897960485217827, + 1.7056626780853676, + 1.7215293076489524, + 1.7373959372125372, + 1.753262566776122, + 1.7691291963397067, + 1.7849958259032914, + 1.8008624554668764, + 1.8167290850304612, + 1.8325957145940461, + 1.8484623441576309, + 1.8643289737212156, + 1.8801956032848004, + 1.8960622328483854, + 1.9119288624119701, + 1.9277954919755549, + 1.9436621215391396, + 1.9595287511027246, + 1.9753953806663094, + 1.9912620102298941, + 2.007128639793479, + 2.0229952693570636, + 2.038861898920649, + 2.054728528484233, + 2.0705951580478184, + 2.086461787611403, + 2.102328417174988, + 2.1181950467385726, + 2.1340616763021574, + 2.1499283058657426, + 2.165794935429327, + 2.181661564992912, + 2.197528194556497, + 2.2133948241200816, + 2.2292614536836663, + 2.245128083247251, + 2.2609947128108363, + 2.2768613423744206, + 2.292727971938006, + 2.3085946015015906, + 2.3244612310651753, + 2.34032786062876, + 2.356194490192345 + ], + "phi": [ + 0.0, + 0.39269908169872414 + ] + }, + { + "type": "DiffractionMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + "Infinity", + "Infinity" + ], + "name": "diffraction", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, + "freqs": [ + 100000000000000.0, + 200000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+" + } + ], + "grid_spec": { + "grid_x": { + "type": "AutoGrid", + "min_steps_per_wvl": 10.0, + "max_scale": 1.4, + "dl_min": 0.0, + "mesher": { + "type": "GradedMesher" + } + }, + "grid_y": { + "type": "CustomGrid", + "dl": [ + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04 + ], + "custom_offset": null + }, + "grid_z": { + "type": "UniformGrid", + "dl": 0.05 + }, + "wavelength": null, + "override_structures": [ + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 2.0, + "conductivity": 0.0 + } + } + ], + "type": "GridSpec" + }, + "version": "2.5.0", + "run_time": 1e-12, + "shutoff": 0.0001, + "subpixel": false, + "normalize_index": 0, + "courant": 0.8 +} \ No newline at end of file diff --git a/tests/sims/simulation_1_10_0rc2.json b/tests/sims/simulation_2_5_0rc1.json similarity index 64% rename from tests/sims/simulation_1_10_0rc2.json rename to tests/sims/simulation_2_5_0rc1.json index 132acc9d9..97404d9ad 100644 --- a/tests/sims/simulation_1_10_0rc2.json +++ b/tests/sims/simulation_2_5_0rc1.json @@ -14,6 +14,9 @@ "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0 @@ -38,11 +41,14 @@ 1.0 ] }, - "name": null, + "name": "dieletric_box", "type": "Structure", "medium": { - "name": null, + "name": "dieletric", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0 @@ -62,11 +68,14 @@ 1.0 ] }, - "name": null, + "name": "lossy_box", "type": "Structure", "medium": { - "name": null, + "name": "lossy_dieletric", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0 @@ -82,11 +91,14 @@ 1.0 ] }, - "name": null, + "name": "sellmeier_sphere", "type": "Structure", "medium": { - "name": null, + "name": "sellmeier", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Sellmeier", "coeffs": [ [ @@ -114,11 +126,14 @@ 1.0 ] }, - "name": null, + "name": "lorentz_box", "type": "Structure", "medium": { - "name": null, + "name": "lorentz", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [ @@ -149,6 +164,35 @@ "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "Debye", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [ @@ -173,11 +217,14 @@ 1.0 ] }, - "name": null, + "name": "drude_box", "type": "Structure", "medium": { - "name": null, + "name": "drude", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [ @@ -188,6 +235,73 @@ ] } }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "Medium2D", + "ss": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + }, + "tt": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + } + } + }, { "geometry": { "type": "GeometryGroup", @@ -207,11 +321,14 @@ } ] }, - "name": null, + "name": "pec_group", "type": "Structure", "medium": { "name": "PEC", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "PECMedium" } }, @@ -229,15 +346,21 @@ ], "length": 2.0 }, - "name": null, + "name": "anisotopic_cylinder", "type": "Structure", "medium": { "name": null, "frequency_range": null, + "allow_gain": null, + "nonlinear_spec": null, + "heat_spec": null, "type": "AnisotropicMedium", "xx": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0 @@ -245,6 +368,9 @@ "yy": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0 @@ -252,6 +378,9 @@ "zz": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 3.0, "conductivity": 0.0 @@ -284,11 +413,295 @@ ] ] }, + "name": "pole_slab", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 6206417594288582.0 + }, + { + "real": -0.0, + "imag": -3.311074436985222e+16 + } + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "CustomMedium", + "interp_method": "nearest", + "subpixel": false, + "permittivity": "SpatialDataArray", + "conductivity": null, + "eps_dataset": null + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, "name": null, "type": "Structure", "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "CustomDrude", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "CustomLorentz", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "CustomDebye", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "CustomPoleResidue", + "eps_inf": "SpatialDataArray", + "poles": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "CustomSellmeier", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": { + "numiters": 20, + "type": "NonlinearSusceptibility", + "chi3": 0.1 + }, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "PolySlab", + "axis": 2, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "slab_bounds": [ + -1.0, + 1.0 + ], + "dilation": 0.0, + "vertices": [ + [ + -1.5, + -1.5 + ], + [ + -0.5, + -1.5 + ], + [ + -0.5, + -0.5 + ] + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [ @@ -313,15 +726,151 @@ "surface_mesh": "TriangleMeshDataArray" } }, + "name": "dieletric_mesh", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 5.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "GeometryGroup", + "geometries": [ + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + } + ] + }, "name": null, "type": "Structure", "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 5.0, "conductivity": 0.0 } + }, + { + "geometry": { + "type": "ClipOperation", + "operation": "symmetric_difference", + "geometry_a": { + "type": "Box", + "center": [ + 0.9, + 0.9, + 0.9 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "geometry_b": { + "type": "Box", + "center": [ + 1.1, + 1.1, + 1.1 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + "name": "clip_operation", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 3.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Transformed", + "geometry": { + "type": "Box", + "center": [ + 1.0, + 1.0, + 1.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "transform": [ + [ + 0.9659258262890683, + -0.25881904510252074, + 0.0, + 0.0 + ], + [ + 0.25881904510252074, + 0.9659258262890683, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "name": "transformed_box", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.5, + "conductivity": 0.0 + } } ], "sources": [ @@ -343,9 +892,11 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, + "interpolate": true, "polarization": "Hx" }, { @@ -366,9 +917,11 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, + "interpolate": true, "polarization": "Ex" }, { @@ -389,7 +942,8 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, "num_freqs": 1, @@ -408,6 +962,7 @@ "bend_radius": null, "bend_axis": null, "track_freq": "central", + "group_index_step": false, "type": "ModeSpec" }, "mode_index": 0 @@ -430,7 +985,8 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, "direction": "+", @@ -456,7 +1012,8 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, "num_freqs": 1, @@ -485,7 +1042,8 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, "num_freqs": 1, @@ -520,7 +1078,8 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, "field_dataset": { @@ -533,6 +1092,39 @@ "Hz": null } }, + { + "type": "CustomCurrentSource", + "center": [ + 0.0, + 1.0, + 2.0 + ], + "size": [ + 2.0, + 2.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "name": null, + "interpolate": true, + "current_dataset": { + "type": "FieldDataset", + "Ex": "ScalarFieldDataArray", + "Ey": null, + "Ez": null, + "Hx": null, + "Hy": null, + "Hz": null + } + }, { "type": "TFSF", "center": [ @@ -551,7 +1143,8 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, "name": null, "direction": "+", @@ -559,6 +1152,34 @@ "angle_phi": 0.6283185307179586, "pol_angle": 0.0, "injection_axis": 2 + }, + { + "type": "UniformCurrentSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "CustomSourceTime", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 0.0, + "source_time_dataset": { + "type": "TimeDataset", + "values": "TimeDataArray" + } + }, + "name": null, + "interpolate": true, + "polarization": "Hx" } ], "boundary_spec": { @@ -633,6 +1254,12 @@ 0.0 ], "name": "field", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 150000000000000.0, 200000000000000.0 @@ -645,13 +1272,7 @@ }, "fields": [ "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false + ] }, { "type": "FieldTimeMonitor", @@ -666,6 +1287,12 @@ 0.0 ], "name": "field_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "start": 0.0, "stop": null, "interval": 100, @@ -676,13 +1303,7 @@ "Hx", "Hy", "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false + ] }, { "type": "FluxMonitor", @@ -697,6 +1318,12 @@ 0.0 ], "name": "flux", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -723,6 +1350,12 @@ 0.0 ], "name": "flux_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "start": 0.0, "stop": null, "interval": 1, @@ -742,6 +1375,12 @@ 0.1 ], "name": "eps", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, "freqs": [ 100000000000000.0 ], @@ -765,6 +1404,12 @@ 0.0 ], "name": "mode", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -789,6 +1434,7 @@ "bend_radius": null, "bend_axis": null, "track_freq": "central", + "group_index_step": false, "type": "ModeSpec" } }, @@ -805,6 +1451,12 @@ 0.0 ], "name": "mode_solver", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -829,8 +1481,10 @@ "bend_radius": null, "bend_axis": null, "track_freq": "central", + "group_index_step": false, "type": "ModeSpec" - } + }, + "direction": "+" }, { "type": "FieldProjectionAngleMonitor", @@ -845,6 +1499,12 @@ 2.0 ], "name": "proj_angle", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -984,6 +1644,12 @@ 2.0 ], "name": "proj_cartesian", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1030,6 +1696,12 @@ 2.0 ], "name": "proj_kspace", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1073,6 +1745,12 @@ 2.0 ], "name": "proj_angle_exact", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1212,6 +1890,12 @@ "Infinity" ], "name": "diffraction", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, "freqs": [ 100000000000000.0, 200000000000000.0 @@ -1366,6 +2050,9 @@ "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0 @@ -1378,5 +2065,5 @@ "subpixel": false, "normalize_index": 0, "courant": 0.8, - "version": "1.10.0rc2" + "version": "2.5.0rc1" } \ No newline at end of file diff --git a/tests/sims/simulation_2_5_0rc2.h5 b/tests/sims/simulation_2_5_0rc2.h5 new file mode 100644 index 000000000..1e92efab4 Binary files /dev/null and b/tests/sims/simulation_2_5_0rc2.h5 differ diff --git a/tests/sims/simulation_1_10_0.json b/tests/sims/simulation_2_5_0rc2.json similarity index 60% rename from tests/sims/simulation_1_10_0.json rename to tests/sims/simulation_2_5_0rc2.json index a977b094a..127b7761a 100644 --- a/tests/sims/simulation_1_10_0.json +++ b/tests/sims/simulation_2_5_0rc2.json @@ -10,19 +10,17 @@ 8.0, 8.0 ], - "run_time": 1e-12, "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0 }, - "symmetry": [ - 0, - 0, - 0 - ], "structures": [ { "geometry": { @@ -38,11 +36,15 @@ 1.0 ] }, - "name": null, + "name": "dieletric_box", "type": "Structure", "medium": { - "name": null, + "name": "dieletric", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0 @@ -62,11 +64,15 @@ 1.0 ] }, - "name": null, + "name": "lossy_box", "type": "Structure", "medium": { - "name": null, + "name": "lossy_dieletric", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 3.0 @@ -82,11 +88,15 @@ 1.0 ] }, - "name": null, + "name": "sellmeier_sphere", "type": "Structure", "medium": { - "name": null, + "name": "sellmeier", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Sellmeier", "coeffs": [ [ @@ -114,11 +124,15 @@ 1.0 ] }, - "name": null, + "name": "lorentz_box", "type": "Structure", "medium": { - "name": null, + "name": "lorentz", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Lorentz", "eps_inf": 2.0, "coeffs": [ @@ -149,6 +163,37 @@ "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Debye", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Debye", "eps_inf": 2.0, "coeffs": [ @@ -173,11 +218,15 @@ 1.0 ] }, - "name": null, + "name": "drude_box", "type": "Structure", "medium": { - "name": null, + "name": "drude", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Drude", "eps_inf": 2.0, "coeffs": [ @@ -188,6 +237,76 @@ ] } }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium2D", + "ss": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + }, + "tt": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + } + } + }, { "geometry": { "type": "GeometryGroup", @@ -207,11 +326,15 @@ } ] }, - "name": null, + "name": "pec_group", "type": "Structure", "medium": { "name": "PEC", "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "PECMedium" } }, @@ -220,7 +343,7 @@ "type": "Cylinder", "axis": 1, "sidewall_angle": 0.0, - "reference_plane": "bottom", + "reference_plane": "middle", "radius": 1.0, "center": [ 1.0, @@ -229,15 +352,23 @@ ], "length": 2.0 }, - "name": null, + "name": "anisotopic_cylinder", "type": "Structure", "medium": { "name": null, "frequency_range": null, + "allow_gain": null, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "AnisotropicMedium", "xx": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0 @@ -245,17 +376,368 @@ "yy": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0 }, - "zz": { - "name": null, - "frequency_range": null, - "type": "Medium", - "permittivity": 3.0, - "conductivity": 0.0 - } + "zz": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 3.0, + "conductivity": 0.0 + } + } + }, + { + "geometry": { + "type": "PolySlab", + "axis": 2, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "slab_bounds": [ + -1.0, + 1.0 + ], + "dilation": 0.0, + "vertices": [ + [ + -1.5, + -1.5 + ], + [ + -0.5, + -1.5 + ], + [ + -0.5, + -0.5 + ] + ] + }, + "name": "pole_slab", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 6206417594288582.0 + }, + { + "real": -0.0, + "imag": -3.311074436985222e+16 + } + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomMedium", + "interp_method": "nearest", + "subpixel": false, + "eps_dataset": null, + "permittivity": "SpatialDataArray", + "conductivity": null + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomDrude", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomLorentz", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomDebye", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomPoleResidue", + "eps_inf": "SpatialDataArray", + "poles": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomSellmeier", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": { + "type": "NonlinearSusceptibility", + "chi3": 0.1, + "numiters": 20 + }, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": { + "models": [ + { + "type": "NonlinearSusceptibility", + "chi3": 0.1, + "numiters": null + }, + { + "type": "TwoPhotonAbsorption", + "beta": { + "real": 1.0, + "imag": 0.0 + }, + "n0": null + }, + { + "type": "KerrNonlinearity", + "n2": { + "real": 1.0, + "imag": 0.0 + }, + "n0": null + } + ], + "num_iters": 10, + "type": "NonlinearSpec" + }, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 } }, { @@ -263,7 +745,7 @@ "type": "PolySlab", "axis": 2, "sidewall_angle": 0.0, - "reference_plane": "bottom", + "reference_plane": "middle", "slab_bounds": [ -1.0, 1.0 @@ -289,6 +771,10 @@ "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "PoleResidue", "eps_inf": 1.0, "poles": [ @@ -313,19 +799,165 @@ "surface_mesh": "TriangleMeshDataArray" } }, + "name": "dieletric_mesh", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 5.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "GeometryGroup", + "geometries": [ + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + } + ] + }, "name": null, "type": "Structure", "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 5.0, "conductivity": 0.0 } + }, + { + "geometry": { + "type": "ClipOperation", + "operation": "symmetric_difference", + "geometry_a": { + "type": "Box", + "center": [ + 0.9, + 0.9, + 0.9 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "geometry_b": { + "type": "Box", + "center": [ + 1.1, + 1.1, + 1.1 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + "name": "clip_operation", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 3.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Transformed", + "geometry": { + "type": "Box", + "center": [ + 1.0, + 1.0, + 1.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "transform": [ + [ + 0.9659258262890683, + -0.25881904510252074, + 0.0, + 0.0 + ], + [ + 0.25881904510252074, + 0.9659258262890683, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "name": "transformed_box", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.5, + "conductivity": 0.0 + } } ], + "symmetry": [ + 0, + 0, + 0 + ], "sources": [ { + "name": null, "type": "UniformCurrentSource", "center": [ 0.0, @@ -343,12 +975,14 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, + "interpolate": true, "polarization": "Hx" }, { + "name": null, "type": "PointDipole", "center": [ 0.0, @@ -366,12 +1000,14 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, + "interpolate": true, "polarization": "Ex" }, { + "name": null, "type": "ModeSource", "center": [ 0.0, @@ -389,9 +1025,9 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, "num_freqs": 1, "direction": "-", "mode_spec": { @@ -408,11 +1044,13 @@ "bend_radius": null, "bend_axis": null, "track_freq": "central", + "group_index_step": false, "type": "ModeSpec" }, "mode_index": 0 }, { + "name": null, "type": "PlaneWave", "center": [ 0.0, @@ -430,15 +1068,16 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, "direction": "+", "angle_theta": 0.0, "angle_phi": 0.0, "pol_angle": 0.1 }, { + "name": null, "type": "GaussianBeam", "center": [ 0.0, @@ -456,9 +1095,9 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, @@ -468,6 +1107,7 @@ "waist_distance": 0.0 }, { + "name": null, "type": "AstigmaticGaussianBeam", "center": [ 0.0, @@ -485,9 +1125,9 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, @@ -503,6 +1143,7 @@ ] }, { + "name": null, "type": "CustomFieldSource", "center": [ 0.0, @@ -520,9 +1161,9 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, "field_dataset": { "type": "FieldDataset", "Ex": "ScalarFieldDataArray", @@ -534,6 +1175,40 @@ } }, { + "name": null, + "type": "CustomCurrentSource", + "center": [ + 0.0, + 1.0, + 2.0 + ], + "size": [ + 2.0, + 2.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "interpolate": true, + "current_dataset": { + "type": "FieldDataset", + "Ex": "ScalarFieldDataArray", + "Ey": null, + "Ez": null, + "Hx": null, + "Hy": null, + "Hz": null + } + }, + { + "name": null, "type": "TFSF", "center": [ 1.0, @@ -551,14 +1226,42 @@ "type": "GaussianPulse", "freq0": 200000000000000.0, "fwidth": 40000000000000.0, - "offset": 5.0 + "offset": 5.0, + "remove_dc_component": true }, - "name": null, "direction": "+", "angle_theta": 0.5235987755982988, "angle_phi": 0.6283185307179586, "pol_angle": 0.0, "injection_axis": 2 + }, + { + "name": null, + "type": "UniformCurrentSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "CustomSourceTime", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 0.0, + "source_time_dataset": { + "type": "TimeDataset", + "values": "TimeDataArray" + } + }, + "interpolate": true, + "polarization": "Hx" } ], "boundary_spec": { @@ -633,6 +1336,12 @@ 0.0 ], "name": "field", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 150000000000000.0, 200000000000000.0 @@ -645,13 +1354,7 @@ }, "fields": [ "Ex" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false + ] }, { "type": "FieldTimeMonitor", @@ -666,6 +1369,12 @@ 0.0 ], "name": "field_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "start": 0.0, "stop": null, "interval": 100, @@ -676,13 +1385,7 @@ "Hx", "Hy", "Hz" - ], - "interval_space": [ - 1, - 1, - 1 - ], - "colocate": false + ] }, { "type": "FluxMonitor", @@ -697,6 +1400,12 @@ 0.0 ], "name": "flux", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -723,6 +1432,12 @@ 0.0 ], "name": "flux_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "start": 0.0, "stop": null, "interval": 1, @@ -742,6 +1457,12 @@ 0.1 ], "name": "eps", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, "freqs": [ 100000000000000.0 ], @@ -765,6 +1486,12 @@ 0.0 ], "name": "mode", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -789,6 +1516,7 @@ "bend_radius": null, "bend_axis": null, "track_freq": "central", + "group_index_step": false, "type": "ModeSpec" } }, @@ -805,6 +1533,12 @@ 0.0 ], "name": "mode_solver", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -829,8 +1563,10 @@ "bend_radius": null, "bend_axis": null, "track_freq": "central", + "group_index_step": false, "type": "ModeSpec" - } + }, + "direction": "+" }, { "type": "FieldProjectionAngleMonitor", @@ -845,6 +1581,12 @@ 2.0 ], "name": "proj_angle", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -984,6 +1726,12 @@ 2.0 ], "name": "proj_cartesian", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1030,6 +1778,12 @@ 2.0 ], "name": "proj_kspace", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1073,6 +1827,12 @@ 2.0 ], "name": "proj_angle_exact", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1212,6 +1972,12 @@ "Infinity" ], "name": "diffraction", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, "freqs": [ 100000000000000.0, 200000000000000.0 @@ -1366,6 +2132,10 @@ "medium": { "name": null, "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 2.0, "conductivity": 0.0 @@ -1374,9 +2144,10 @@ ], "type": "GridSpec" }, + "version": "2.5.0rc2", + "run_time": 1e-12, "shutoff": 0.0001, "subpixel": false, "normalize_index": 0, - "courant": 0.8, - "version": "1.10.0" + "courant": 0.8 } \ No newline at end of file diff --git a/tests/sims/simulation_2_5_0rc3.json b/tests/sims/simulation_2_5_0rc3.json new file mode 100644 index 000000000..a2d5ab982 --- /dev/null +++ b/tests/sims/simulation_2_5_0rc3.json @@ -0,0 +1,2270 @@ +{ + "type": "Simulation", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 8.0, + 8.0, + 8.0 + ], + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + }, + "structures": [ + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": "dieletric_box", + "type": "Structure", + "medium": { + "name": "dieletric", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 2.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + "Infinity", + 1.0 + ] + }, + "name": "lossy_box", + "type": "Structure", + "medium": { + "name": "lossy_dieletric", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 3.0 + } + }, + { + "geometry": { + "type": "Sphere", + "radius": 1.0, + "center": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": "sellmeier_sphere", + "type": "Structure", + "medium": { + "name": "sellmeier", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Sellmeier", + "coeffs": [ + [ + 1.03961212, + 0.00600069867 + ], + [ + 0.231792344, + 0.0200179144 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": "lorentz_box", + "type": "Structure", + "medium": { + "name": "lorentz", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Lorentz", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 2.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Debye", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Debye", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": "drude_box", + "type": "Structure", + "medium": { + "name": "drude", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Drude", + "eps_inf": 2.0, + "coeffs": [ + [ + 1.0, + 3.0 + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium2D", + "ss": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + }, + "tt": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 0.0 + }, + { + "real": 254117040158918.28, + "imag": 0.0 + } + ] + ] + } + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 0.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium2D", + "ss": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + }, + "tt": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + } + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": null, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "AnisotropicMedium", + "xx": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + }, + "yy": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + }, + "zz": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + } + }, + { + "geometry": { + "type": "GeometryGroup", + "geometries": [ + { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + } + ] + }, + "name": "pec_group", + "type": "Structure", + "medium": { + "name": "PEC", + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PECMedium" + } + }, + { + "geometry": { + "type": "Cylinder", + "axis": 1, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "radius": 1.0, + "center": [ + 1.0, + 0.0, + -1.0 + ], + "length": 2.0 + }, + "name": "anisotopic_cylinder", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": null, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "AnisotropicMedium", + "xx": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + }, + "yy": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 2.0, + "conductivity": 0.0 + }, + "zz": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 3.0, + "conductivity": 0.0 + } + } + }, + { + "geometry": { + "type": "PolySlab", + "axis": 2, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "slab_bounds": [ + -1.0, + 1.0 + ], + "dilation": 0.0, + "vertices": [ + [ + -1.5, + -1.5 + ], + [ + -0.5, + -1.5 + ], + [ + -0.5, + -0.5 + ] + ] + }, + "name": "pole_slab", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 6206417594288582.0 + }, + { + "real": -0.0, + "imag": -3.311074436985222e+16 + } + ] + ] + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomMedium", + "interp_method": "nearest", + "subpixel": false, + "eps_dataset": null, + "permittivity": "SpatialDataArray", + "conductivity": null + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomDrude", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomLorentz", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomDebye", + "eps_inf": "SpatialDataArray", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomPoleResidue", + "eps_inf": "SpatialDataArray", + "poles": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "CustomSellmeier", + "coeffs": [ + [ + "SpatialDataArray", + "SpatialDataArray" + ] + ], + "interp_method": "nearest", + "subpixel": false + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": { + "type": "NonlinearSusceptibility", + "chi3": 0.1, + "numiters": 20 + }, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.5, + 0.5 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": { + "models": [ + { + "type": "NonlinearSusceptibility", + "chi3": 0.1, + "numiters": null + }, + { + "type": "TwoPhotonAbsorption", + "beta": { + "real": 1.0, + "imag": 0.0 + }, + "n0": null + }, + { + "type": "KerrNonlinearity", + "n2": { + "real": 1.0, + "imag": 0.0 + }, + "n0": null + } + ], + "num_iters": 10, + "type": "NonlinearSpec" + }, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "PolySlab", + "axis": 2, + "sidewall_angle": 0.0, + "reference_plane": "middle", + "slab_bounds": [ + -1.0, + 1.0 + ], + "dilation": 0.0, + "vertices": [ + [ + -1.5, + -1.5 + ], + [ + -0.5, + -1.5 + ], + [ + -0.5, + -0.5 + ] + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "PoleResidue", + "eps_inf": 1.0, + "poles": [ + [ + { + "real": 0.0, + "imag": 6206417594288582.0 + }, + { + "real": -0.0, + "imag": -3.311074436985222e+16 + } + ] + ] + } + }, + { + "geometry": { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + "name": "dieletric_mesh", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 5.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "GeometryGroup", + "geometries": [ + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + }, + { + "type": "TriangleMesh", + "mesh_dataset": { + "type": "TriangleMeshDataset", + "surface_mesh": "TriangleMeshDataArray" + } + } + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 5.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "ClipOperation", + "operation": "symmetric_difference", + "geometry_a": { + "type": "Box", + "center": [ + 0.9, + 0.9, + 0.9 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "geometry_b": { + "type": "Box", + "center": [ + 1.1, + 1.1, + 1.1 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + } + }, + "name": "clip_operation", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 3.0, + "conductivity": 0.0 + } + }, + { + "geometry": { + "type": "Transformed", + "geometry": { + "type": "Box", + "center": [ + 1.0, + 1.0, + 1.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "transform": [ + [ + 0.9659258262890683, + -0.25881904510252074, + 0.0, + 0.0 + ], + [ + 0.25881904510252074, + 0.9659258262890683, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ] + }, + "name": "transformed_box", + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 1.5, + "conductivity": 0.0 + } + } + ], + "symmetry": [ + 0, + 0, + 0 + ], + "sources": [ + { + "name": null, + "type": "UniformCurrentSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "interpolate": true, + "polarization": "Hx" + }, + { + "name": null, + "type": "PointDipole", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0, + 0, + 0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "interpolate": true, + "polarization": "Ex" + }, + { + "name": null, + "type": "ModeSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 2.0, + 0.0, + 2.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "num_freqs": 1, + "direction": "-", + "mode_spec": { + "num_modes": 1, + "target_neff": null, + "num_pml": [ + 0, + 0 + ], + "filter_pol": null, + "angle_theta": 0.0, + "angle_phi": 0.0, + "precision": "single", + "bend_radius": null, + "bend_axis": null, + "track_freq": "central", + "group_index_step": false, + "type": "ModeSpec" + }, + "mode_index": 0 + }, + { + "name": null, + "type": "PlaneWave", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + "Infinity", + "Infinity" + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "direction": "+", + "angle_theta": 0.0, + "angle_phi": 0.0, + "pol_angle": 0.1 + }, + { + "name": null, + "type": "GaussianBeam", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 3.0, + 3.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "num_freqs": 1, + "direction": "+", + "angle_theta": 0.0, + "angle_phi": 0.0, + "pol_angle": 1.5707963267948966, + "waist_radius": 1.0, + "waist_distance": 0.0 + }, + { + "name": null, + "type": "AstigmaticGaussianBeam", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 3.0, + 3.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "num_freqs": 1, + "direction": "+", + "angle_theta": 0.0, + "angle_phi": 0.0, + "pol_angle": 1.5707963267948966, + "waist_sizes": [ + 1.0, + 2.0 + ], + "waist_distances": [ + 3.0, + 4.0 + ] + }, + { + "name": null, + "type": "CustomFieldSource", + "center": [ + 0.0, + 1.0, + 2.0 + ], + "size": [ + 2.0, + 2.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "field_dataset": { + "type": "FieldDataset", + "Ex": "ScalarFieldDataArray", + "Ey": null, + "Ez": null, + "Hx": null, + "Hy": null, + "Hz": null + } + }, + { + "name": null, + "type": "CustomCurrentSource", + "center": [ + 0.0, + 1.0, + 2.0 + ], + "size": [ + 2.0, + 2.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "interpolate": true, + "current_dataset": { + "type": "FieldDataset", + "Ex": "ScalarFieldDataArray", + "Ey": null, + "Ez": null, + "Hx": null, + "Hy": null, + "Hz": null + } + }, + { + "name": null, + "type": "TFSF", + "center": [ + 1.0, + 2.0, + -3.0 + ], + "size": [ + 2.5, + 2.5, + 0.5 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "GaussianPulse", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 5.0, + "remove_dc_component": true + }, + "direction": "+", + "angle_theta": 0.5235987755982988, + "angle_phi": 0.6283185307179586, + "pol_angle": 0.0, + "injection_axis": 2 + }, + { + "name": null, + "type": "UniformCurrentSource", + "center": [ + 0.0, + 0.5, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "source_time": { + "amplitude": 1.0, + "phase": 0.0, + "type": "CustomSourceTime", + "freq0": 200000000000000.0, + "fwidth": 40000000000000.0, + "offset": 0.0, + "source_time_dataset": { + "type": "TimeDataset", + "values": "TimeDataArray" + } + }, + "interpolate": true, + "polarization": "Hx" + } + ], + "boundary_spec": { + "x": { + "plus": { + "name": null, + "type": "PML", + "num_layers": 20, + "parameters": { + "sigma_order": 3, + "sigma_min": 0.0, + "sigma_max": 1.5, + "type": "PMLParams", + "kappa_order": 3, + "kappa_min": 1.0, + "kappa_max": 3.0, + "alpha_order": 1, + "alpha_min": 0.0, + "alpha_max": 0.0 + } + }, + "minus": { + "name": null, + "type": "Absorber", + "num_layers": 100, + "parameters": { + "sigma_order": 3, + "sigma_min": 0.0, + "sigma_max": 6.4, + "type": "AbsorberParams" + } + }, + "type": "Boundary" + }, + "y": { + "plus": { + "name": null, + "type": "BlochBoundary", + "bloch_vec": 1.0 + }, + "minus": { + "name": null, + "type": "BlochBoundary", + "bloch_vec": 1.0 + }, + "type": "Boundary" + }, + "z": { + "plus": { + "name": null, + "type": "Periodic" + }, + "minus": { + "name": null, + "type": "Periodic" + }, + "type": "Boundary" + }, + "type": "BoundarySpec" + }, + "monitors": [ + { + "type": "FieldMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "name": "field", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 150000000000000.0, + 200000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "fields": [ + "Ex" + ] + }, + { + "type": "FieldTimeMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 0.0, + 0.0 + ], + "name": "field_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "start": 0.0, + "stop": null, + "interval": 100, + "fields": [ + "Ex", + "Ey", + "Ez", + "Hx", + "Hy", + "Hz" + ] + }, + { + "type": "FluxMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "flux", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 200000000000000.0, + 250000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null + }, + { + "type": "FluxTimeMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "flux_time", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "start": 0.0, + "stop": null, + "interval": 1, + "normal_dir": "+", + "exclude_surfaces": null + }, + { + "type": "PermittivityMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.1 + ], + "name": "eps", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, + "freqs": [ + 100000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + } + }, + { + "type": "ModeMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "mode", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, + "freqs": [ + 200000000000000.0, + 250000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "mode_spec": { + "num_modes": 1, + "target_neff": null, + "num_pml": [ + 0, + 0 + ], + "filter_pol": null, + "angle_theta": 0.0, + "angle_phi": 0.0, + "precision": "single", + "bend_radius": null, + "bend_axis": null, + "track_freq": "central", + "group_index_step": false, + "type": "ModeSpec" + } + }, + { + "type": "ModeSolverMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 0.0 + ], + "name": "mode_solver", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 200000000000000.0, + 250000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "mode_spec": { + "num_modes": 1, + "target_neff": null, + "num_pml": [ + 0, + 0 + ], + "filter_pol": null, + "angle_theta": 0.0, + "angle_phi": 0.0, + "precision": "single", + "bend_radius": null, + "bend_axis": null, + "track_freq": "central", + "group_index_step": false, + "type": "ModeSpec" + }, + "direction": "+" + }, + { + "type": "FieldProjectionAngleMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_angle", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "proj_distance": 1000000.0, + "theta": [ + 0.7853981633974483, + 0.8012647929610331, + 0.8171314225246179, + 0.8329980520882028, + 0.8488646816517875, + 0.8647313112153724, + 0.8805979407789571, + 0.896464570342542, + 0.9123311999061268, + 0.9281978294697116, + 0.9440644590332964, + 0.9599310885968813, + 0.975797718160466, + 0.9916643477240509, + 1.0075309772876357, + 1.0233976068512205, + 1.0392642364148053, + 1.05513086597839, + 1.070997495541975, + 1.0868641251055597, + 1.1027307546691445, + 1.1185973842327295, + 1.1344640137963142, + 1.150330643359899, + 1.1661972729234837, + 1.1820639024870687, + 1.1979305320506535, + 1.2137971616142382, + 1.2296637911778232, + 1.245530420741408, + 1.2613970503049927, + 1.2772636798685775, + 1.2931303094321622, + 1.3089969389957472, + 1.324863568559332, + 1.340730198122917, + 1.3565968276865017, + 1.3724634572500864, + 1.3883300868136712, + 1.404196716377256, + 1.4200633459408407, + 1.4359299755044257, + 1.4517966050680104, + 1.4676632346315954, + 1.4835298641951802, + 1.499396493758765, + 1.5152631233223497, + 1.5311297528859344, + 1.5469963824495194, + 1.5628630120131042, + 1.5787296415766892, + 1.594596271140274, + 1.6104629007038587, + 1.6263295302674434, + 1.6421961598310282, + 1.658062789394613, + 1.673929418958198, + 1.6897960485217827, + 1.7056626780853676, + 1.7215293076489524, + 1.7373959372125372, + 1.753262566776122, + 1.7691291963397067, + 1.7849958259032914, + 1.8008624554668764, + 1.8167290850304612, + 1.8325957145940461, + 1.8484623441576309, + 1.8643289737212156, + 1.8801956032848004, + 1.8960622328483854, + 1.9119288624119701, + 1.9277954919755549, + 1.9436621215391396, + 1.9595287511027246, + 1.9753953806663094, + 1.9912620102298941, + 2.007128639793479, + 2.0229952693570636, + 2.038861898920649, + 2.054728528484233, + 2.0705951580478184, + 2.086461787611403, + 2.102328417174988, + 2.1181950467385726, + 2.1340616763021574, + 2.1499283058657426, + 2.165794935429327, + 2.181661564992912, + 2.197528194556497, + 2.2133948241200816, + 2.2292614536836663, + 2.245128083247251, + 2.2609947128108363, + 2.2768613423744206, + 2.292727971938006, + 2.3085946015015906, + 2.3244612310651753, + 2.34032786062876, + 2.356194490192345 + ], + "phi": [ + 0.0, + 0.5235987755982988 + ] + }, + { + "type": "FieldProjectionCartesianMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_cartesian", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "proj_axis": 2, + "proj_distance": 5.0, + "x": [ + -1.0, + 0.0, + 1.0 + ], + "y": [ + -2.0, + -1.0, + 0.0, + 1.0, + 2.0 + ] + }, + { + "type": "FieldProjectionKSpaceMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_kspace", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "proj_axis": 2, + "proj_distance": 1000000.0, + "ux": [ + 0.02, + 0.04 + ], + "uy": [ + 0.03, + 0.04, + 0.05 + ] + }, + { + "type": "FieldProjectionAngleMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + 2.0, + 2.0 + ], + "name": "proj_angle_exact", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": true, + "freqs": [ + 250000000000000.0, + 300000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+", + "exclude_surfaces": null, + "custom_origin": [ + 1.0, + 2.0, + 3.0 + ], + "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], + "proj_distance": 1000000.0, + "theta": [ + 0.7853981633974483, + 0.8012647929610331, + 0.8171314225246179, + 0.8329980520882028, + 0.8488646816517875, + 0.8647313112153724, + 0.8805979407789571, + 0.896464570342542, + 0.9123311999061268, + 0.9281978294697116, + 0.9440644590332964, + 0.9599310885968813, + 0.975797718160466, + 0.9916643477240509, + 1.0075309772876357, + 1.0233976068512205, + 1.0392642364148053, + 1.05513086597839, + 1.070997495541975, + 1.0868641251055597, + 1.1027307546691445, + 1.1185973842327295, + 1.1344640137963142, + 1.150330643359899, + 1.1661972729234837, + 1.1820639024870687, + 1.1979305320506535, + 1.2137971616142382, + 1.2296637911778232, + 1.245530420741408, + 1.2613970503049927, + 1.2772636798685775, + 1.2931303094321622, + 1.3089969389957472, + 1.324863568559332, + 1.340730198122917, + 1.3565968276865017, + 1.3724634572500864, + 1.3883300868136712, + 1.404196716377256, + 1.4200633459408407, + 1.4359299755044257, + 1.4517966050680104, + 1.4676632346315954, + 1.4835298641951802, + 1.499396493758765, + 1.5152631233223497, + 1.5311297528859344, + 1.5469963824495194, + 1.5628630120131042, + 1.5787296415766892, + 1.594596271140274, + 1.6104629007038587, + 1.6263295302674434, + 1.6421961598310282, + 1.658062789394613, + 1.673929418958198, + 1.6897960485217827, + 1.7056626780853676, + 1.7215293076489524, + 1.7373959372125372, + 1.753262566776122, + 1.7691291963397067, + 1.7849958259032914, + 1.8008624554668764, + 1.8167290850304612, + 1.8325957145940461, + 1.8484623441576309, + 1.8643289737212156, + 1.8801956032848004, + 1.8960622328483854, + 1.9119288624119701, + 1.9277954919755549, + 1.9436621215391396, + 1.9595287511027246, + 1.9753953806663094, + 1.9912620102298941, + 2.007128639793479, + 2.0229952693570636, + 2.038861898920649, + 2.054728528484233, + 2.0705951580478184, + 2.086461787611403, + 2.102328417174988, + 2.1181950467385726, + 2.1340616763021574, + 2.1499283058657426, + 2.165794935429327, + 2.181661564992912, + 2.197528194556497, + 2.2133948241200816, + 2.2292614536836663, + 2.245128083247251, + 2.2609947128108363, + 2.2768613423744206, + 2.292727971938006, + 2.3085946015015906, + 2.3244612310651753, + 2.34032786062876, + 2.356194490192345 + ], + "phi": [ + 0.0, + 0.39269908169872414 + ] + }, + { + "type": "DiffractionMonitor", + "center": [ + 0.0, + 0.0, + 0.0 + ], + "size": [ + 0.0, + "Infinity", + "Infinity" + ], + "name": "diffraction", + "interval_space": [ + 1, + 1, + 1 + ], + "colocate": false, + "freqs": [ + 100000000000000.0, + 200000000000000.0 + ], + "apodization": { + "start": null, + "end": null, + "width": null, + "type": "ApodizationSpec" + }, + "normal_dir": "+" + } + ], + "grid_spec": { + "grid_x": { + "type": "AutoGrid", + "min_steps_per_wvl": 10.0, + "max_scale": 1.4, + "dl_min": 0.0, + "mesher": { + "type": "GradedMesher" + } + }, + "grid_y": { + "type": "CustomGrid", + "dl": [ + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04, + 0.04 + ], + "custom_offset": null + }, + "grid_z": { + "type": "UniformGrid", + "dl": 0.05 + }, + "wavelength": null, + "override_structures": [ + { + "geometry": { + "type": "Box", + "center": [ + -1.0, + 0.0, + 0.0 + ], + "size": [ + 1.0, + 1.0, + 1.0 + ] + }, + "name": null, + "type": "Structure", + "medium": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, + "type": "Medium", + "permittivity": 2.0, + "conductivity": 0.0 + } + } + ], + "type": "GridSpec" + }, + "version": "2.5.0rc3", + "run_time": 1e-12, + "shutoff": 0.0001, + "subpixel": false, + "normalize_index": 0, + "courant": 0.8 +} \ No newline at end of file diff --git a/tests/test_components/test_IO.py b/tests/test_components/test_IO.py index 2bd770081..b0b4ba623 100644 --- a/tests/test_components/test_IO.py +++ b/tests/test_components/test_IO.py @@ -15,12 +15,22 @@ from ..utils import SIM_MONITORS as SIM2 from ..test_data.test_monitor_data import make_flux_data from ..test_data.test_sim_data import make_sim_data +from ..utils import run_emulated + from tidy3d.components.data.sim_data import DATA_TYPE_MAP # Store an example of every minor release simulation to test updater in the future SIM_DIR = "tests/sims" +@pytest.fixture +def split_string(monkeypatch): + """Lower the max string length in hdf5 read/write, in order to test the string splitting.""" + from tidy3d.components import base + + monkeypatch.setattr(base, "MAX_STRING_LENGTH", 100) + + def set_datasets_to_none(sim): sim_dict = sim.dict() for src in sim_dict["sources"]: @@ -47,7 +57,7 @@ def set_datasets_to_none(sim): return td.Simulation.parse_obj(sim_dict) -def test_simulation_load_export(): +def test_simulation_load_export(split_string): major, minor, patch = __version__.split(".") path = os.path.join(SIM_DIR, f"simulation_{major}_{minor}_{patch}.json") # saving as .h5 since *.hdf5 is git ignored @@ -56,7 +66,9 @@ def test_simulation_load_export(): SIM.to_hdf5(path_hdf5) SIM2 = td.Simulation.from_file(path) SIM_HDF5 = td.Simulation.from_hdf5(path_hdf5) - assert set_datasets_to_none(SIM) == SIM2, "original and loaded simulations are not the same" + assert ( + set_datasets_to_none(SIM)._json_string == SIM2._json_string + ), "original and loaded simulations are not the same" assert SIM == SIM_HDF5, "original and loaded from hdf5 simulations are not the same" @@ -64,7 +76,9 @@ def test_simulation_load_export_yaml(tmp_path): path = str(tmp_path / "simulation.yaml") SIM.to_file(path) SIM2 = td.Simulation.from_file(path) - assert set_datasets_to_none(SIM) == SIM2, "original and loaded simulations are not the same" + assert ( + set_datasets_to_none(SIM)._json_string == SIM2._json_string + ), "original and loaded simulations are not the same" def test_component_load_export(tmp_path): @@ -81,28 +95,28 @@ def test_component_load_export_yaml(tmp_path): assert td.Medium() == M2, "original and loaded medium are not the same" -def test_simulation_load_export_hdf5(tmp_path): +def test_simulation_load_export_hdf5(split_string, tmp_path): path = str(tmp_path / "simulation.hdf5") SIM.to_file(path) SIM2 = td.Simulation.from_file(path) assert SIM == SIM2, "original and loaded simulations are not the same" -def test_simulation_load_export_hdf5_gz(tmp_path): +def test_simulation_load_export_hdf5_gz(split_string, tmp_path): path = str(tmp_path / "simulation.hdf5.gz") SIM.to_file(path) SIM2 = td.Simulation.from_file(path) assert SIM == SIM2, "original and loaded simulations are not the same" -def test_simulation_load_export_hdf5_explicit(tmp_path): +def test_simulation_load_export_hdf5_explicit(split_string, tmp_path): path = str(tmp_path / "simulation.hdf5") SIM.to_hdf5(path) SIM2 = td.Simulation.from_hdf5(path) assert SIM == SIM2, "original and loaded simulations are not the same" -def test_simulation_load_export_hdf5_gz_explicit(tmp_path): +def test_simulation_load_export_hdf5_gz_explicit(split_string, tmp_path): path = str(tmp_path / "simulation.hdf5.gz") SIM.to_hdf5_gz(path) SIM2 = td.Simulation.from_hdf5_gz(path) @@ -123,7 +137,7 @@ def test_simulation_preserve_types(tmp_path): path = str(tmp_path / "simulation.json") SIM.to_file(path) sim_2 = td.Simulation.from_file(path) - assert set_datasets_to_none(SIM) == sim_2 + assert set_datasets_to_none(SIM)._json_string == sim_2._json_string M_types = [type(s.medium) for s in sim_2.structures] for M in (td.Medium, td.PoleResidue, td.Lorentz, td.Sellmeier, td.Debye): @@ -178,7 +192,7 @@ def test_validation_speed(tmp_path): _S = td.Simulation.from_file(path) time_validate = time() - time_start times_sec.append(time_validate) - assert set_datasets_to_none(S) == _S + assert set_datasets_to_none(S)._json_string == _S._json_string size = os.path.getsize(path) sizes_bytes.append(size) @@ -266,3 +280,39 @@ def test_group_name_tuple(): assert index == true_index group_name = tidy.get_tuple_group_name(index=index) assert group_name == key_name + + +def test_monitor_data_from_file(): + """Test the ability to load specific monitor data from a file.""" + + sim = td.Simulation( + size=(1, 1, 1), + grid_spec=td.GridSpec.auto(wavelength=1.0), + monitors=[ + td.FieldMonitor(center=(0, 0, 0), size=(1, 1, 0), freqs=[2e14], name="field"), + td.ModeMonitor( + center=(0, 0, 0), size=(1, 1, 0), freqs=[2e14], mode_spec=td.ModeSpec(), name="mode" + ), + ], + sources=[ + td.PointDipole( + center=(0, 0, 0), + polarization="Ex", + source_time=td.GaussianPulse(freq0=2e14, fwidth=1e13), + ) + ], + run_time=2e-12, + ) + + sim_data = run_emulated(sim, task_name="test") + + fname = "tests/data/sim_data.hdf5" + sim_data.to_file(fname) + + fld_data = td.SimulationData.mnt_data_from_file(fname, mnt_name="field") + assert isinstance(fld_data, td.FieldData) + assert fld_data.monitor == sim.monitors[0] + + mode_data = td.SimulationData.mnt_data_from_file(fname, mnt_name="mode") + assert isinstance(mode_data, td.ModeData) + assert mode_data.monitor == sim.monitors[1] diff --git a/tests/test_components/test_base.py b/tests/test_components/test_base.py index def94d906..08099b4a9 100644 --- a/tests/test_components/test_base.py +++ b/tests/test_components/test_base.py @@ -115,3 +115,12 @@ def test_updated_copy(): assert s2.medium == m2 s3 = s.updated_copy(**{"medium": m2, "geometry": b2}) assert s3 == s2 + + +def test_equality(): + + # test freqs / arraylike + mnt1 = td.FluxMonitor(size=(1, 1, 0), freqs=np.array([1, 2, 3]) * 1e12, name="1") + mnt2 = td.FluxMonitor(size=(1, 1, 0), freqs=np.array([1, 2, 3]) * 1e12, name="1") + + assert mnt1 == mnt2 diff --git a/tests/test_components/test_bc_placement.py b/tests/test_components/test_bc_placement.py new file mode 100644 index 000000000..d2ed82b3c --- /dev/null +++ b/tests/test_components/test_bc_placement.py @@ -0,0 +1,20 @@ +import pytest +import pydantic.v1 as pd +import numpy as np + +import tidy3d as td +from tidy3d.components.bc_placement import ( + StructureBoundary, + StructureStructureInterface, + SimulationBoundary, + StructureSimulationBoundary, + MediumMediumInterface, +) + + +def test_bc_placement(): + _ = StructureBoundary(structure="box") + _ = SimulationBoundary() + _ = StructureSimulationBoundary(structure="box") + _ = StructureStructureInterface(structures=["box", "sphere"]) + _ = MediumMediumInterface(mediums=["dieletric", "metal"]) diff --git a/tests/test_components/test_field_projection.py b/tests/test_components/test_field_projection.py index 20119be42..8e1208292 100644 --- a/tests/test_components/test_field_projection.py +++ b/tests/test_components/test_field_projection.py @@ -92,7 +92,28 @@ def make_proj_monitors(center, size, freqs): far_field_approx=False, ) - return n2f_angle_monitor, n2f_cart_monitor, n2f_ksp_monitor, exact_cart_monitor + downsampled_cart_monitor = td.FieldProjectionCartesianMonitor( + center=center, + size=size, + freqs=freqs, + name="downsampled_cart", + custom_origin=center, + x=list(xs), + y=list(ys), + proj_axis=proj_axis, + proj_distance=z, + normal_dir="+", + exclude_surfaces=exclude_surfaces, + interval_space=(1, 2, 3), + ) + + return ( + n2f_angle_monitor, + n2f_cart_monitor, + n2f_ksp_monitor, + exact_cart_monitor, + downsampled_cart_monitor, + ) def test_proj_monitors(): @@ -253,7 +274,7 @@ def test_proj_clientside(): center = (0, 0, 0) size = (2, 2, 0) - f0 = 1 + f0 = 1e13 monitor = td.FieldMonitor(size=size, center=center, freqs=[f0], name="near_field") sim_size = (5, 5, 5) @@ -292,9 +313,13 @@ def test_proj_clientside(): ) # make near-to-far monitors - n2f_angle_monitor, n2f_cart_monitor, n2f_ksp_monitor, exact_cart_monitor = make_proj_monitors( - center, size, [f0] - ) + ( + n2f_angle_monitor, + n2f_cart_monitor, + n2f_ksp_monitor, + exact_cart_monitor, + _, + ) = make_proj_monitors(center, size, [f0]) far_fields_angular = proj.project_fields(n2f_angle_monitor) far_fields_cartesian = proj.project_fields(n2f_cart_monitor) diff --git a/tests/test_components/test_geometry.py b/tests/test_components/test_geometry.py index 04330a9ff..ad0568213 100644 --- a/tests/test_components/test_geometry.py +++ b/tests/test_components/test_geometry.py @@ -50,6 +50,10 @@ geometry_b=td.Box(center=(0.25, 0, 0), size=(0.5, 1, 1)), ), ) +TRANSFORMED = td.Transformed( + geometry=BOX, + transform=td.Transformed.rotation(np.pi / 6, 0), +) GEO_TYPES = [ @@ -62,6 +66,7 @@ DIFFERENCE, SYM_DIFFERENCE, GROUP, + TRANSFORMED, ] _, AX = plt.subplots() @@ -258,7 +263,7 @@ def test_polyslab_inf_bounds(lower_bound, upper_bound): # catch any runtime warning related to inf operations with warnings.catch_warnings(): warnings.simplefilter("error") - bounds = ps.bounds + _ = ps.bounds ps.intersections_plane(x=0.5) ps.intersections_plane(z=0) @@ -286,7 +291,7 @@ def test_surfaces(): td.Box.surfaces(size=(1, 0, 1), center=(0, 0, 0)) td.FluxMonitor.surfaces( - size=(1, 1, 1), center=(0, 0, 0), normal_dir="+", name="test", freqs=[1] + size=(1, 1, 1), center=(0, 0, 0), normal_dir="+", name="test", freqs=[1e12] ) td.Box.surfaces(size=(1, 1, 1), center=(0, 0, 0), normal_dir="+") @@ -395,6 +400,58 @@ def test_geometryoperations(): ) +@pytest.mark.parametrize("axis", [0, 1, 2]) +def test_planar_transform(axis): + geo = ( + td.Box(size=(3 * axis, 2 * abs(axis - 1), 4 * (2 - axis))) + .rotated(2.0, axis) + .translated(-1, 2, 3) + .scaled(1.4, -1.2, 1.3) + ) + assert geo.bounds[0][axis] == geo.bounds[1][axis] + + +def test_transforms(): + xyz = (np.array([1.4, 0]), np.array([0, 0.5]), np.array([0, 1.4])) + geo = td.Box(size=(2, 2, 2)) + assert not geo.inside(*xyz).any() + geo = geo.rotated(np.pi / 4, 2).rotated(np.pi / 5, 0) + geo.plot(x=0) + assert geo.inside(*xyz).all() + + xyz = (np.array([0, 0, -1.5 + 1e-6]), np.array([0, 0, 0]), np.array([-1e-6, 4 - 1e-6, 2])) + geo = td.Sphere(radius=1) + assert (geo.inside(*xyz) == (True, False, False)).all() + geo = geo.translated(0, 0, 1).scaled(1.5, 1, 2) + geo.plot(y=0) + assert (geo.inside(*xyz) == (False, True, True)).all() + + xyz = (np.array([0.8, -0.8, -0.7]), np.array([0, 0, 0]), np.array([1.2, -1.2, 0])) + geo = td.Cylinder(length=2, radius=1) + assert (geo.inside(*xyz) == (False, False, True)).all() + geo = geo.scaled(0.5, 2, 1).rotated(-np.pi / 6, 2).rotated(np.pi / 2, 0) + assert (geo.inside(*xyz) == (True, True, False)).all() + + xyz = (np.array([0, 2, 1, 3, -0.5]), np.array([0, 0, 0, 0, 0.5]), np.array([0, 0, 1.5, 0, 0])) + geo = geo = td.PolySlab( + vertices=[(2, -1), (-1, 1), (4, 1), (-1, 2), (4, 2), (1, 3), (5, 3), (5, -1)], + slab_bounds=(-1, 1), + ) + assert (geo.inside(*xyz) == (False, True, False, True, False)).all() + assert len(geo.intersections_plane(x=0)) == 2 + assert len(geo.intersections_plane(z=0)) == 1 + geo = geo.translated(-2, 0, 0).rotated(-np.pi * 0.4, 1) + assert (geo.inside(*xyz) == (True, False, True, False, True)).all() + assert len(geo.intersections_plane(x=0)) == 1 + assert len(geo.intersections_plane(z=0)) == 3 + + +def test_general_rotation(): + assert np.allclose(td.Transformed.rotation(0.1, 0), td.Transformed.rotation(0.1, [2, 0, 0])) + assert np.allclose(td.Transformed.rotation(0.2, 1), td.Transformed.rotation(0.2, [0, 3, 0])) + assert np.allclose(td.Transformed.rotation(0.3, 2), td.Transformed.rotation(0.3, [0, 0, 4])) + + def test_flattening(): flat = list( flatten_groups( @@ -692,6 +749,21 @@ def test_from_gds(): assert len(geo.intersections_plane(z=1)) == 1 +@pytest.mark.parametrize("geometry", GEO_TYPES) +def test_to_gds(geometry, tmp_path): + fname = str(tmp_path / f"{geometry.__class__.__name__}.gds") + geometry.to_gds_file(fname, z=0, gds_cell_name=geometry.__class__.__name__) + cell = gdstk.read_gds(fname).cells[0] + assert cell.name == geometry.__class__.__name__ + assert len(cell.polygons) > 0 + + fname = str(tmp_path / f"{geometry.__class__.__name__}-empty.gds") + geometry.to_gds_file(fname, y=1e30, gds_cell_name=geometry.__class__.__name__) + cell = gdstk.read_gds(fname).cells[0] + assert cell.name == geometry.__class__.__name__ + assert len(cell.polygons) == 0 + + def test_custom_surface_geometry(tmp_path): # create tetrahedron STL vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]]) diff --git a/tests/test_components/test_heat.py b/tests/test_components/test_heat.py new file mode 100644 index 000000000..c864703a6 --- /dev/null +++ b/tests/test_components/test_heat.py @@ -0,0 +1,431 @@ +import pytest +import pydantic.v1 as pd +import numpy as np +from matplotlib import pyplot as plt + +import tidy3d as td + +from tidy3d import FluidSpec, SolidSpec +from tidy3d import UniformHeatSource +from tidy3d import ( + TemperatureBC, + HeatFluxBC, + ConvectionBC, + HeatBoundarySpec, +) +from tidy3d import ( + StructureBoundary, + StructureStructureInterface, + SimulationBoundary, + StructureSimulationBoundary, + MediumMediumInterface, +) +from tidy3d import UniformUnstructuredGrid, DistanceUnstructuredGrid +from tidy3d import HeatSimulation +from tidy3d import HeatSimulationData +from tidy3d import TemperatureMonitor +from tidy3d import TemperatureData +from tidy3d.exceptions import DataError + +from ..utils import STL_GEO, assert_log_level, log_capture + + +def make_heat_mediums(): + fluid_medium = td.Medium( + permittivity=3, + heat_spec=FluidSpec(), + name="fluid_medium", + ) + solid_medium = td.Medium( + permittivity=5, + conductivity=0.01, + heat_spec=SolidSpec( + capacity=2, + conductivity=3, + ), + name="solid_medium", + ) + + return fluid_medium, solid_medium + + +def test_heat_medium(): + _, solid_medium = make_heat_mediums() + + with pytest.raises(pd.ValidationError): + _ = solid_medium.heat_spec.updated_copy(capacity=-1) + + with pytest.raises(pd.ValidationError): + _ = solid_medium.heat_spec.updated_copy(conductivity=-1) + + +def make_heat_structures(): + fluid_medium, solid_medium = make_heat_mediums() + + box = td.Box(center=(0, 0, 0), size=(1, 1, 1)) + + fluid_structure = td.Structure( + geometry=box, + medium=fluid_medium, + name="fluid_structure", + ) + + solid_structure = td.Structure( + geometry=box.updated_copy(center=(1, 1, 1)), + medium=solid_medium, + name="solid_structure", + ) + + return fluid_structure, solid_structure + + +def test_heat_structures(): + _, _ = make_heat_structures() + + +def make_heat_bcs(): + bc_temp = TemperatureBC(temperature=300) + bc_flux = HeatFluxBC(flux=20) + bc_conv = ConvectionBC(ambient_temperature=400, transfer_coeff=0.2) + + return bc_temp, bc_flux, bc_conv + + +def test_heat_bcs(): + bc_temp, bc_flux, bc_conv = make_heat_bcs() + + with pytest.raises(pd.ValidationError): + _ = TemperatureBC(temperature=-10) + + with pytest.raises(pd.ValidationError): + _ = ConvectionBC(ambient_temperature=-400, transfer_coeff=0.2) + + with pytest.raises(pd.ValidationError): + _ = ConvectionBC(ambient_temperature=400, transfer_coeff=-0.2) + + +def make_heat_mnts(): + temp_mnt1 = TemperatureMonitor(size=(1, 2, 3), name="test") + temp_mnt2 = TemperatureMonitor(size=(1, 2, 3), name="tet", unstructured=True) + temp_mnt3 = TemperatureMonitor(size=(1, 0, 3), name="tri", unstructured=True, conformal=True) + temp_mnt4 = TemperatureMonitor(size=(1, 0, 3), name="empty", unstructured=True, conformal=False) + + return (temp_mnt1, temp_mnt2, temp_mnt3, temp_mnt4) + + +def test_heat_mnt(): + temp_mnt, _, _, _ = make_heat_mnts() + + with pytest.raises(pd.ValidationError): + _ = temp_mnt.updated_copy(name=None) + + with pytest.raises(pd.ValidationError): + _ = temp_mnt.updated_copy(size=(-1, 2, 3)) + + +def make_heat_mnt_data(): + temp_mnt1, temp_mnt2, temp_mnt3, temp_mnt4 = make_heat_mnts() + + nx, ny, nz = 9, 6, 5 + x = np.linspace(0, 1, nx) + y = np.linspace(0, 2, ny) + z = np.linspace(0, 3, nz) + T = np.random.default_rng().uniform(300, 350, (nx, ny, nz)) + coords = dict(x=x, y=y, z=z) + temperature_field = td.SpatialDataArray(T, coords=coords) + + mnt_data1 = TemperatureData(monitor=temp_mnt1, temperature=temperature_field) + + tet_grid_points = td.PointDataArray( + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + dims=("index", "axis"), + ) + + tet_grid_cells = td.CellDataArray( + [[0, 1, 2, 4], [1, 2, 3, 4]], + dims=("cell_index", "vertex_index"), + ) + + tet_grid_values = td.IndexedDataArray( + [1.0, 2.0, 3.0, 4.0, 5.0], + dims=("index"), + name="T", + ) + + tet_grid = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells, + values=tet_grid_values, + ) + + mnt_data2 = TemperatureData(monitor=temp_mnt2, temperature=tet_grid) + + tri_grid_points = td.PointDataArray( + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], + dims=("index", "axis"), + ) + + tri_grid_cells = td.CellDataArray( + [[0, 1, 2], [1, 2, 3]], + dims=("cell_index", "vertex_index"), + ) + + tri_grid_values = td.IndexedDataArray( + [1.0, 2.0, 3.0, 4.0], + dims=("index"), + name="T", + ) + + tri_grid = td.TriangularGridDataset( + normal_axis=1, + normal_pos=0, + points=tri_grid_points, + cells=tri_grid_cells, + values=tri_grid_values, + ) + + mnt_data3 = TemperatureData(monitor=temp_mnt3, temperature=tri_grid) + + mnt_data4 = TemperatureData(monitor=temp_mnt4, temperature=None) + + return (mnt_data1, mnt_data2, mnt_data3, mnt_data4) + + +def test_heat_mnt_data(): + _ = make_heat_mnt_data() + + +def make_uniform_grid_spec(): + return UniformUnstructuredGrid(dl=0.1, min_edges_per_circumference=5, min_edges_per_side=3) + + +def make_distance_grid_spec(): + return DistanceUnstructuredGrid( + dl_interface=0.1, dl_bulk=1, distance_interface=1, distance_bulk=2 + ) + + +def test_grid_spec(): + grid_spec = make_uniform_grid_spec() + with pytest.raises(pd.ValidationError): + _ = grid_spec.updated_copy(dl=0) + with pytest.raises(pd.ValidationError): + _ = grid_spec.updated_copy(min_edges_per_circumference=-1) + with pytest.raises(pd.ValidationError): + _ = grid_spec.updated_copy(min_edges_per_side=-1) + + grid_spec = make_distance_grid_spec() + with pytest.raises(pd.ValidationError): + grid_spec.updated_copy(dl_interface=-1) + with pytest.raises(pd.ValidationError): + grid_spec.updated_copy(distance_interface=2, distance_bulk=1) + + +def make_heat_source(): + return UniformHeatSource(structures=["solid_structure"], rate=100) + + +def test_heat_source(): + _ = make_heat_source() + + +def make_heat_sim(): + fluid_medium, solid_medium = make_heat_mediums() + fluid_structure, solid_structure = make_heat_structures() + bc_temp, bc_flux, bc_conv = make_heat_bcs() + heat_source = make_heat_source() + + pl1 = HeatBoundarySpec( + condition=bc_conv, placement=MediumMediumInterface(mediums=["fluid_medium", "solid_medium"]) + ) + pl2 = HeatBoundarySpec( + condition=bc_flux, placement=StructureBoundary(structure="solid_structure") + ) + pl3 = HeatBoundarySpec( + condition=bc_flux, + placement=StructureStructureInterface(structures=["fluid_structure", "solid_structure"]), + ) + pl4 = HeatBoundarySpec(condition=bc_temp, placement=SimulationBoundary()) + pl5 = HeatBoundarySpec( + condition=bc_temp, placement=StructureSimulationBoundary(structure="fluid_structure") + ) + + grid_spec = make_uniform_grid_spec() + + temp_mnts = make_heat_mnts() + + heat_sim = HeatSimulation( + medium=fluid_medium, + structures=[fluid_structure, solid_structure], + center=(0, 0, 0), + size=(2, 2, 2), + boundary_spec=[pl1, pl2, pl3, pl4, pl5], + grid_spec=grid_spec, + sources=[heat_source], + monitors=temp_mnts, + ) + + return heat_sim + + +def test_heat_sim(): + bc_temp, bc_flux, bc_conv = make_heat_bcs() + heat_sim = make_heat_sim() + + _ = heat_sim.plot(x=0) + + # wrong names given + for pl in [ + HeatBoundarySpec( + condition=bc_temp, placement=MediumMediumInterface(mediums=["badname", "fluid_medium"]) + ), + HeatBoundarySpec(condition=bc_flux, placement=StructureBoundary(structure="no_box")), + HeatBoundarySpec( + condition=bc_conv, + placement=StructureStructureInterface(structures=["no_box", "solid_structure"]), + ), + HeatBoundarySpec( + condition=bc_temp, placement=StructureSimulationBoundary(structure="no_mesh") + ), + ]: + with pytest.raises(pd.ValidationError): + _ = heat_sim.updated_copy(boundary_spec=[pl]) + + with pytest.raises(pd.ValidationError): + _ = heat_sim.updated_copy(sources=[UniformHeatSource(structures=["noname"])], rate=-10) + + # polyslab support + vertices = np.array([(0, 0), (1, 0), (1, 1)]) + p = td.PolySlab(vertices=vertices, axis=2, slab_bounds=(-1, 1)) + _, structure = make_heat_structures() + structure = structure.updated_copy(geometry=p, name="polyslab") + _ = heat_sim.updated_copy(structures=list(heat_sim.structures) + [structure]) + + # test unsupported yet geometries + structure = structure.updated_copy(geometry=STL_GEO, name="stl") + with pytest.raises(pd.ValidationError): + _ = heat_sim.updated_copy(structures=list(heat_sim.structures) + [structure]) + + # test unsupported yet zero dimension domains + with pytest.raises(pd.ValidationError): + _ = heat_sim.updated_copy(center=(0, 0, 0), size=(0, 2, 2)) + + with pytest.raises(pd.ValidationError): + _ = heat_sim.updated_copy(center=(0, 0, 0), size=(1, 0, 0)) + + temp_mnt = heat_sim.monitors[0] + + with pytest.raises(pd.ValidationError): + heat_sim.updated_copy(monitors=[temp_mnt, temp_mnt]) + + _ = heat_sim.plot(x=0) + plt.close() + + _ = heat_sim.plot_heat_conductivity(y=0) + plt.close() + + heat_sim = heat_sim.updated_copy(symmetry=(0, 1, 1)) + _ = heat_sim.plot_heat_conductivity(z=0, colorbar="source") + plt.close() + + with pytest.raises(pd.ValidationError): + _ = heat_sim.updated_copy(symmetry=(-1, 0, 1)) + + +@pytest.mark.parametrize("shift_amount, log_level", ((1, None), (2, "WARNING"))) +def test_heat_sim_bounds(shift_amount, log_level, log_capture): + """make sure bounds are working correctly""" + + # make sure all things are shifted to this central location + CENTER_SHIFT = (-1.0, 1.0, 100.0) + + def place_box(center_offset): + + shifted_center = tuple(c + s for (c, s) in zip(center_offset, CENTER_SHIFT)) + + _ = td.HeatSimulation( + size=(1.5, 1.5, 1.5), + center=CENTER_SHIFT, + structures=[ + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=shifted_center), medium=td.Medium() + ) + ], + grid_spec=td.UniformUnstructuredGrid(dl=0.1), + ) + + # create all permutations of squares being shifted 1, -1, or zero in all three directions + bin_strings = [list(format(i, "03b")) for i in range(8)] + bin_ints = [[int(b) for b in bin_string] for bin_string in bin_strings] + bin_ints = np.array(bin_ints) + bin_signs = 2 * (bin_ints - 0.5) + + # test all cases where box is shifted +/- 1 in x,y,z and still intersects + for amp in bin_ints: + for sign in bin_signs: + center = shift_amount * amp * sign + if np.sum(center) < 1e-12: + continue + place_box(tuple(center)) + assert_log_level(log_capture, log_level) + + +@pytest.mark.parametrize( + "box_size,log_level", + [ + ((1, 0.1, 0.1), "WARNING"), + ((0.1, 1, 0.1), "WARNING"), + ((0.1, 0.1, 1), "WARNING"), + ], +) +def test_sim_structure_extent(log_capture, box_size, log_level): + """Make sure we warn if structure extends exactly to simulation edges.""" + + box = td.Structure(geometry=td.Box(size=box_size), medium=td.Medium(permittivity=2)) + _ = td.HeatSimulation( + size=(1, 1, 1), + structures=[box], + grid_spec=td.UniformUnstructuredGrid(dl=0.1), + ) + + assert_log_level(log_capture, log_level) + + +def make_heat_sim_data(): + heat_sim = make_heat_sim() + temp_data = make_heat_mnt_data() + + heat_sim_data = HeatSimulationData( + simulation=heat_sim, + data=temp_data, + ) + + return heat_sim_data + + +def test_sim_data(): + heat_sim_data = make_heat_sim_data() + _ = heat_sim_data.plot_field("test", z=0) + _ = heat_sim_data.plot_field("tri") + _ = heat_sim_data.plot_field("tet", y=0.5) + plt.close() + + with pytest.raises(DataError): + _ = heat_sim_data.plot_field("empty") + + with pytest.raises(DataError): + _ = heat_sim_data.plot_field("test") + + with pytest.raises(KeyError): + _ = heat_sim_data.plot_field("test3", x=0) + + with pytest.raises(pd.ValidationError): + _ = heat_sim_data.updated_copy(data=[heat_sim_data.data[0]] * 2) + + temp_mnt = TemperatureMonitor(size=(1, 2, 3), name="test") + temp_mnt = temp_mnt.updated_copy(name="test2") + + sim = heat_sim_data.simulation.updated_copy(monitors=[temp_mnt]) + + with pytest.raises(pd.ValidationError): + _ = heat_sim_data.updated_copy(simulation=sim) diff --git a/tests/test_components/test_medium.py b/tests/test_components/test_medium.py index 69cf7053c..d78e90af2 100644 --- a/tests/test_components/test_medium.py +++ b/tests/test_components/test_medium.py @@ -4,7 +4,7 @@ import pydantic.v1 as pydantic import matplotlib.pyplot as plt import tidy3d as td -from tidy3d.exceptions import ValidationError +from tidy3d.exceptions import ValidationError, SetupError from ..utils import assert_log_level, log_capture from typing import Dict @@ -113,6 +113,17 @@ def test_medium_dispersion(): eps_c = medium.eps_model(freqs) assert np.all(eps_c.imag >= 0) + # test eps_model for int arguments + m_SM.eps_model(np.array([1, 2])) + + # test LO-TO form + poles = [(1, 0.1, 2, 5), (3, 0.4, 1, 0.4)] + m_LO_TO = td.PoleResidue.from_lo_to(poles=poles, eps_inf=2) + assert np.allclose( + m_LO_TO.eps_model(freqs), + td.PoleResidue.lo_to_eps_model(poles=poles, eps_inf=2, frequency=freqs), + ) + def test_medium_dispersion_conversion(): @@ -439,6 +450,9 @@ def test_fully_anisotropic_media(): rotation=rot, ) + # check eps_model can be called with an array of frequencies + eps = m.eps_model(np.linspace(1e12, 2e12, 10)) + assert np.allclose(m.permittivity, perm) assert np.allclose(m.conductivity, cond) @@ -543,13 +557,120 @@ def test_perturbation_medium(): ) -def test_nonlinear_medium(): - med = td.Medium(nonlinear_spec=td.NonlinearSusceptibility(chi3=1.5, numiters=20)) +def test_nonlinear_medium(log_capture): + med = td.Medium( + nonlinear_spec=td.NonlinearSpec( + models=[ + td.NonlinearSusceptibility(chi3=1.5), + td.TwoPhotonAbsorption(beta=1), + td.KerrNonlinearity(n2=1), + ], + num_iters=20, + ) + ) - with pytest.raises(pydantic.ValidationError): - med = td.PoleResidue( - poles=[(-1, 1)], nonlinear_spec=td.NonlinearSusceptibility(chi3=1.5, numiters=20) + # complex parameters + med = td.Medium( + nonlinear_spec=td.NonlinearSpec( + models=[ + td.KerrNonlinearity(n2=-1 + 1j, n0=1), + td.TwoPhotonAbsorption(beta=1 + 1j, n0=1), + ], + num_iters=20, + ) + ) + assert_log_level(log_capture, None) + + # warn about deprecated api + med = td.Medium(nonlinear_spec=td.NonlinearSusceptibility(chi3=1.5)) + assert_log_level(log_capture, "WARNING") + + # don't use deprecated numiters + with pytest.raises(ValidationError): + med = td.Medium( + nonlinear_spec=td.NonlinearSpec(models=[td.NonlinearSusceptibility(chi3=1, numiters=2)]) + ) + + # dispersive support + med = td.PoleResidue(poles=[(-1, 1)], nonlinear_spec=td.NonlinearSusceptibility(chi3=1.5)) + + # unsupported material types + with pytest.raises(ValidationError): + med = td.AnisotropicMedium( + xx=med, yy=med, zz=med, nonlinear_spec=td.NonlinearSusceptibility(chi3=1.5) ) + # numiters too large with pytest.raises(pydantic.ValidationError): med = td.Medium(nonlinear_spec=td.NonlinearSusceptibility(chi3=1.5, numiters=200)) + with pytest.raises(pydantic.ValidationError): + med = td.Medium( + nonlinear_spec=td.NonlinearSpec( + num_iters=200, models=[td.NonlinearSusceptibility(chi3=1.5)] + ) + ) + + # duplicate models + with pytest.raises(pydantic.ValidationError): + med = td.Medium( + nonlinear_spec=td.NonlinearSpec( + models=[ + td.NonlinearSusceptibility(chi3=1.5), + td.NonlinearSusceptibility(chi3=1), + ] + ) + ) + + # active materials + with pytest.raises(ValidationError): + med = td.Medium( + nonlinear_spec=td.NonlinearSpec(models=[td.TwoPhotonAbsorption(beta=-1 + 1j, n0=1)]) + ) + + with pytest.raises(ValidationError): + med = td.Medium(nonlinear_spec=td.NonlinearSpec(models=[td.KerrNonlinearity(n2=-1j, n0=1)])) + + med = td.Medium( + nonlinear_spec=td.NonlinearSpec(models=[td.TwoPhotonAbsorption(beta=-1, n0=1)]), + allow_gain=True, + ) + + # automatic detection of n0 + n0 = 2 + freq0 = td.C_0 / 1 + nonlinear_spec = td.NonlinearSpec(models=[td.KerrNonlinearity(n2=1)]) + medium = td.Sellmeier.from_dispersion(n=n0, freq=freq0, dn_dwvl=-0.2).updated_copy( + nonlinear_spec=nonlinear_spec + ) + source_time = td.GaussianPulse(freq0=freq0, fwidth=freq0 / 10) + source = td.PointDipole(center=(0, 0, 0), source_time=source_time, polarization="Ex") + monitor = td.FieldMonitor(size=(td.inf, td.inf, 0), freqs=[freq0], name="field") + structure = td.Structure(geometry=td.Box(size=(5, 5, 5)), medium=medium) + sim = td.Simulation( + size=(10, 10, 10), + run_time=1e-12, + grid_spec=td.GridSpec.uniform(dl=0.1), + sources=[source], + monitors=[monitor], + structures=[structure], + ) + assert n0 == nonlinear_spec.models[0]._get_n0(n0=None, medium=medium, freqs=[freq0]) + + # can't detect n0 with different source freqs + source_time2 = source_time.updated_copy(freq0=2 * freq0) + source2 = source.updated_copy(source_time=source_time2) + with pytest.raises(SetupError): + sim.updated_copy(sources=[source, source2]) + + # but if we provided it, it's ok + nonlinear_spec = td.NonlinearSpec(models=[td.KerrNonlinearity(n2=1, n0=1)]) + structure = structure.updated_copy(medium=medium.updated_copy(nonlinear_spec=nonlinear_spec)) + sim = sim.updated_copy(structures=[structure]) + assert 1 == nonlinear_spec.models[0]._get_n0(n0=1, medium=medium, freqs=[1, 2]) + + # active materials with automatic detection of n0 + nonlinear_spec_active = td.NonlinearSpec(models=[td.TwoPhotonAbsorption(beta=-1)]) + medium_active = medium.updated_copy(nonlinear_spec=nonlinear_spec_active) + with pytest.raises(ValidationError): + structure = structure.updated_copy(medium=medium_active) + sim.updated_copy(structures=[structure]) diff --git a/tests/test_components/test_monitor.py b/tests/test_components/test_monitor.py index f04171eba..22f68a412 100644 --- a/tests/test_components/test_monitor.py +++ b/tests/test_components/test_monitor.py @@ -197,6 +197,26 @@ def test_fieldproj_local_origin(): M.local_origin +def test_fieldproj_window(): + M = td.FieldProjectionAngleMonitor( + size=(2, 0, 2), theta=[1, 2], phi=[0], name="f", freqs=[2e12], window_size=(0.2, 1) + ) + window_size, window_minus, window_plus = M.window_parameters() + window_size, window_minus, window_plus = M.window_parameters(M.bounds) + points = np.linspace(0, 10, 100) + _ = M.window_function(points, window_size, window_minus, window_plus, 2) + # do not allow a window size larger than 1 + with pytest.raises(pydantic.ValidationError): + _ = td.FieldProjectionAngleMonitor( + size=(2, 0, 2), theta=[1, 2], phi=[0], name="f", freqs=[2e12], window_size=(0.2, 1.1) + ) + # do not allow non-zero windows for volume monitors + with pytest.raises(pydantic.ValidationError): + _ = td.FieldProjectionAngleMonitor( + size=(2, 1, 2), theta=[1, 2], phi=[0], name="f", freqs=[2e12], window_size=(0.2, 0) + ) + + PROJ_MNTS = [ td.FieldProjectionAngleMonitor(size=(2, 0, 2), theta=[1, 2], phi=[0], name="f", freqs=[2e12]), td.FieldProjectionCartesianMonitor( @@ -230,16 +250,16 @@ def test_monitor_colocate(log_capture): monitor = td.FieldMonitor( size=(td.inf, td.inf, td.inf), - freqs=np.linspace(0, 200e12, 1001), + freqs=np.linspace(1e12, 200e12, 1001), name="test", interval_space=(1, 2, 3), ) assert monitor.colocate is True - assert_log_level(log_capture, "WARNING") + assert_log_level(log_capture, None) monitor = td.FieldMonitor( size=(td.inf, td.inf, td.inf), - freqs=np.linspace(0, 200e12, 1001), + freqs=np.linspace(1e12, 200e12, 1001), name="test", interval_space=(1, 2, 3), colocate=False, @@ -247,13 +267,15 @@ def test_monitor_colocate(log_capture): assert monitor.colocate is False -@pytest.mark.parametrize("freqs, log_level", [(np.arange(2500), "WARNING"), (np.arange(100), None)]) +@pytest.mark.parametrize( + "freqs, log_level", [(np.arange(1, 2500), "WARNING"), (np.arange(1, 100), None)] +) def test_monitor_num_freqs(log_capture, freqs, log_level): """test default colocate value, and warning if not set""" monitor = td.FieldMonitor( size=(td.inf, td.inf, td.inf), - freqs=freqs, + freqs=freqs * 1e12, name="test", colocate=True, ) @@ -296,28 +318,31 @@ def test_diffraction_validators(): _ = td.DiffractionMonitor(size=[td.inf, 4, 0], freqs=[1e12], name="de") +FREQS = np.array([1, 2, 3]) * 1e12 + + def test_monitor(): size = (1, 2, 3) center = (1, 2, 3) - m1 = td.FieldMonitor(size=size, center=center, freqs=[1, 2, 3], name="test_monitor") - _ = td.FieldMonitor.surfaces(size=size, center=center, freqs=[1, 2, 3], name="test_monitor") + m1 = td.FieldMonitor(size=size, center=center, freqs=FREQS, name="test_monitor") + _ = td.FieldMonitor.surfaces(size=size, center=center, freqs=FREQS, name="test_monitor") m2 = td.FieldTimeMonitor(size=size, center=center, name="test_mon") - m3 = td.FluxMonitor(size=(1, 1, 0), center=center, freqs=[1, 2, 3], name="test_mon") + m3 = td.FluxMonitor(size=(1, 1, 0), center=center, freqs=FREQS, name="test_mon") m4 = td.FluxTimeMonitor(size=(1, 1, 0), center=center, name="test_mon") m5 = td.ModeMonitor( - size=(1, 1, 0), center=center, mode_spec=td.ModeSpec(), freqs=[1, 2, 3], name="test_mon" + size=(1, 1, 0), center=center, mode_spec=td.ModeSpec(), freqs=FREQS, name="test_mon" ) m6 = td.ModeSolverMonitor( size=(1, 1, 0), center=center, mode_spec=td.ModeSpec(), - freqs=[1, 2, 3], + freqs=FREQS, name="test_mon", direction="-", ) - m7 = td.PermittivityMonitor(size=size, center=center, freqs=[1, 2, 3], name="perm") + m7 = td.PermittivityMonitor(size=size, center=center, freqs=FREQS, name="perm") tmesh = np.linspace(0, 1, 10) @@ -333,16 +358,14 @@ def test_monitor(): def test_monitor_plane(): - freqs = [1, 2, 3] - # make sure flux, mode and diffraction monitors fail with non planar geometries for size in ((0, 0, 0), (1, 0, 0), (1, 1, 1)): with pytest.raises(pydantic.ValidationError): - td.ModeMonitor(size=size, freqs=freqs, modes=[]) + td.ModeMonitor(size=size, freqs=FREQS, modes=[]) with pytest.raises(pydantic.ValidationError): - td.ModeSolverMonitor(size=size, freqs=freqs, modes=[]) + td.ModeSolverMonitor(size=size, freqs=FREQS, modes=[]) with pytest.raises(pydantic.ValidationError): - td.DiffractionMonitor(size=size, freqs=freqs, name="de") + td.DiffractionMonitor(size=size, freqs=FREQS, name="de") def _test_freqs_nonempty(): @@ -357,14 +380,12 @@ def test_monitor_surfaces_from_volume(): # make sure that monitors with zero volume raise an error (adapted from test_monitor_plane()) for size in ((0, 0, 0), (1, 0, 0), (1, 1, 0)): with pytest.raises(SetupError): - _ = td.FieldMonitor.surfaces( - size=size, center=center, freqs=[1, 2, 3], name="test_monitor" - ) + _ = td.FieldMonitor.surfaces(size=size, center=center, freqs=FREQS, name="test_monitor") # test that the surface monitors can be extracted from a volume monitor size = (1, 2, 3) monitor_surfaces = td.FieldMonitor.surfaces( - size=size, center=center, freqs=[1, 2, 3], name="test_monitor" + size=size, center=center, freqs=FREQS, name="test_monitor" ) # x- surface diff --git a/tests/test_components/test_scene.py b/tests/test_components/test_scene.py new file mode 100644 index 000000000..d54ba52fb --- /dev/null +++ b/tests/test_components/test_scene.py @@ -0,0 +1,281 @@ +"""Tests the scene and its validators.""" +import pytest +import pydantic.v1 as pd +import matplotlib.pyplot as plt + +import numpy as np +import tidy3d as td +from tidy3d.components.scene import MAX_NUM_MEDIUMS, MAX_GEOMETRY_COUNT +from ..utils import assert_log_level, log_capture, SIM_FULL + +SCENE = td.Scene() + +SCENE_FULL = SIM_FULL.scene + + +def test_scene_init(): + """make sure a scene can be initialized""" + + scene = td.Scene( + structures=[ + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), + medium=td.Medium(permittivity=2.0), + ), + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)), + medium=td.Medium(permittivity=1.0, conductivity=3.0), + ), + td.Structure( + geometry=td.Sphere(radius=1.4, center=(1.0, 0.0, 1.0)), medium=td.Medium() + ), + td.Structure( + geometry=td.Cylinder(radius=1.4, length=2.0, center=(1.0, 0.0, -1.0), axis=1), + medium=td.Medium(), + ), + ], + medium=td.Medium(permittivity=3.0), + ) + + _ = scene.mediums + _ = scene.medium_map + _ = scene.background_structure + + +def test_validate_components_none(): + + assert SCENE._validate_num_mediums(val=None) is None + + +def test_plot_eps(): + ax = SCENE_FULL.plot_eps(x=0) + SCENE_FULL._add_cbar_eps(eps_min=1, eps_max=2, ax=ax) + plt.close() + + +def test_plot_eps_bounds(): + _ = SCENE_FULL.plot_eps(x=0, hlim=[-0.45, 0.45]) + plt.close() + _ = SCENE_FULL.plot_eps(x=0, vlim=[-0.45, 0.45]) + plt.close() + _ = SCENE_FULL.plot_eps(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45]) + plt.close() + + +def test_plot(): + SCENE_FULL.plot(x=0) + plt.close() + + +def test_plot_1d_scene(): + s = td.Scene(structures=[td.Structure(geometry=td.Box(size=(0, 0, 1)), medium=td.Medium())]) + _ = s.plot(y=0) + plt.close() + + +def test_plot_bounds(): + _ = SCENE_FULL.plot(x=0, hlim=[-0.45, 0.45]) + plt.close() + _ = SCENE_FULL.plot(x=0, vlim=[-0.45, 0.45]) + plt.close() + _ = SCENE_FULL.plot(x=0, hlim=[-0.45, 0.45], vlim=[-0.45, 0.45]) + plt.close() + + +def test_structure_alpha(): + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=None) + plt.close() + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=-1) + plt.close() + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=1) + plt.close() + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=0.5) + plt.close() + _ = SCENE_FULL.plot_structures_eps(x=0, alpha=0.5, cbar=True) + plt.close() + new_structs = [ + td.Structure(geometry=s.geometry, medium=SCENE_FULL.medium) for s in SCENE_FULL.structures + ] + S2 = SCENE_FULL.copy(update=dict(structures=new_structs)) + _ = S2.plot_structures_eps(x=0, alpha=0.5) + plt.close() + + +def test_filter_structures(): + s1 = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=SCENE.medium) + s2 = td.Structure(geometry=td.Box(size=(1, 1, 1), center=(1, 1, 1)), medium=SCENE.medium) + plane = td.Box(center=(0, 0, 1.5), size=(td.inf, td.inf, 0)) + SCENE._filter_structures_plane_medium(structures=[s1, s2], plane=plane) + + +def test_get_structure_plot_params(): + pp = SCENE_FULL._get_structure_plot_params(mat_index=0, medium=SCENE_FULL.medium) + assert pp.facecolor == "white" + pp = SCENE_FULL._get_structure_plot_params(mat_index=1, medium=td.PEC) + assert pp.facecolor == "gold" + pp = SCENE_FULL._get_structure_eps_plot_params( + medium=SCENE_FULL.medium, freq=1, eps_min=1, eps_max=2 + ) + assert float(pp.facecolor) == 1.0 + pp = SCENE_FULL._get_structure_eps_plot_params(medium=td.PEC, freq=1, eps_min=1, eps_max=2) + assert pp.facecolor == "gold" + + +def test_num_mediums(): + """Make sure we error if too many mediums supplied.""" + + structures = [] + for i in range(MAX_NUM_MEDIUMS): + structures.append( + td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=td.Medium(permittivity=i + 1)) + ) + _ = td.Scene( + structures=structures, + ) + + with pytest.raises(pd.ValidationError): + structures.append( + td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=td.Medium(permittivity=i + 2)) + ) + _ = td.Scene(structures=structures) + + +def _test_names_default(): + """makes sure default names are set""" + + scene = td.Scene( + size=(2.0, 2.0, 2.0), + structures=[ + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), + medium=td.Medium(permittivity=2.0), + ), + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)), + medium=td.Medium(permittivity=2.0), + ), + td.Structure( + geometry=td.Sphere(radius=1.4, center=(1.0, 0.0, 1.0)), medium=td.Medium() + ), + td.Structure( + geometry=td.Cylinder(radius=1.4, length=2.0, center=(1.0, 0.0, -1.0), axis=1), + medium=td.Medium(), + ), + ], + ) + + for i, structure in enumerate(scene.structures): + assert structure.name == f"structures[{i}]" + + +def test_names_unique(): + + with pytest.raises(pd.ValidationError): + _ = td.Scene( + structures=[ + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), + medium=td.Medium(permittivity=2.0), + name="struct1", + ), + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)), + medium=td.Medium(permittivity=2.0), + name="struct1", + ), + ], + ) + + +def test_perturbed_mediums_copy(): + + # Non-dispersive + pp_real = td.ParameterPerturbation( + heat=td.LinearHeatPerturbation( + coeff=-0.01, + temperature_ref=300, + temperature_range=(200, 500), + ), + ) + + pp_complex = td.ParameterPerturbation( + heat=td.LinearHeatPerturbation( + coeff=0.01j, + temperature_ref=300, + temperature_range=(200, 500), + ), + charge=td.LinearChargePerturbation( + electron_coeff=-1e-21, + electron_ref=0, + electron_range=(0, 1e20), + hole_coeff=-2e-21, + hole_ref=0, + hole_range=(0, 0.5e20), + ), + ) + + coords = dict(x=[1, 2], y=[3, 4], z=[5, 6]) + temperature = td.SpatialDataArray(300 * np.ones((2, 2, 2)), coords=coords) + electron_density = td.SpatialDataArray(1e18 * np.ones((2, 2, 2)), coords=coords) + hole_density = td.SpatialDataArray(2e18 * np.ones((2, 2, 2)), coords=coords) + + pmed1 = td.PerturbationMedium(permittivity=3, permittivity_perturbation=pp_real) + + pmed2 = td.PerturbationPoleResidue( + poles=[(1j, 3), (2j, 4)], + poles_perturbation=[(None, pp_real), (pp_complex, None)], + ) + + struct = td.Structure(geometry=td.Box(center=(0, 0, 0), size=(1, 1, 1)), medium=pmed2) + + scene = td.Scene( + medium=pmed1, + structures=[struct], + ) + + # no perturbations provided -> regular mediums + new_scene = scene.perturbed_mediums_copy() + + assert isinstance(new_scene.medium, td.Medium) + assert isinstance(new_scene.structures[0].medium, td.PoleResidue) + + # perturbations provided -> custom mediums + new_scene = scene.perturbed_mediums_copy(temperature) + new_scene = scene.perturbed_mediums_copy(temperature, None, hole_density) + new_scene = scene.perturbed_mediums_copy(temperature, electron_density, hole_density) + + assert isinstance(new_scene.medium, td.CustomMedium) + assert isinstance(new_scene.structures[0].medium, td.CustomPoleResidue) + + +def test_max_geometry_validation(): + too_many = [td.Box(size=(1, 1, 1)) for _ in range(MAX_GEOMETRY_COUNT + 1)] + + fine = [ + td.Structure( + geometry=td.ClipOperation( + operation="union", + geometry_a=td.Box(size=(1, 1, 1)), + geometry_b=td.GeometryGroup(geometries=too_many), + ), + medium=td.Medium(permittivity=2.0), + ), + td.Structure( + geometry=td.GeometryGroup(geometries=too_many), + medium=td.Medium(permittivity=2.0), + ), + ] + _ = td.Scene(structures=fine) + + not_fine = [ + td.Structure( + geometry=td.ClipOperation( + operation="difference", + geometry_a=td.Box(size=(1, 1, 1)), + geometry_b=td.GeometryGroup(geometries=too_many), + ), + medium=td.Medium(permittivity=2.0), + ), + ] + with pytest.raises(pd.ValidationError, match=f" {MAX_GEOMETRY_COUNT + 2} "): + _ = td.Scene(structures=not_fine) diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index dbecfa120..a735635d3 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -2,12 +2,14 @@ import pytest import pydantic.v1 as pydantic import matplotlib.pyplot as plt +import gdstk import numpy as np import tidy3d as td from tidy3d.exceptions import SetupError, ValidationError, Tidy3dKeyError from tidy3d.components import simulation -from tidy3d.components.simulation import MAX_NUM_MEDIUMS, MAX_NUM_SOURCES +from tidy3d.components.simulation import MAX_NUM_SOURCES +from tidy3d.components.scene import MAX_NUM_MEDIUMS, MAX_GEOMETRY_COUNT from ..utils import assert_log_level, SIM_FULL, log_capture, run_emulated from tidy3d.constants import LARGE_NUMBER @@ -60,7 +62,7 @@ def test_sim_init(): ), ], monitors=[ - td.FieldMonitor(size=(0, 0, 0), center=(0, 0, 0), freqs=[1, 2], name="point"), + td.FieldMonitor(size=(0, 0, 0), center=(0, 0, 0), freqs=[1e12, 2e12], name="point"), td.FluxTimeMonitor(size=(1, 1, 0), center=(0, 0, 0), interval=10, name="plane"), ], symmetry=(0, 1, -1), @@ -77,20 +79,25 @@ def test_sim_init(): _ = sim.dt _ = sim.tmesh sim.validate_pre_upload() + m = sim.get_monitor_by_name("point") + # will not work in 3.0 _ = sim.mediums _ = sim.medium_map - m = sim.get_monitor_by_name("point") _ = sim.background_structure + # will continue working in 3.0 + _ = sim.scene.mediums + _ = sim.scene.medium_map + _ = sim.scene.background_structure # sim.plot(x=0) # plt.close() # sim.plot_eps(x=0) # plt.close() - sim.num_pml_layers + _ = sim.num_pml_layers # sim.plot_grid(x=0) # plt.close() - sim.frequency_range - sim.grid - sim.num_cells + _ = sim.frequency_range + _ = sim.grid + _ = sim.num_cells sim.discretize(m) sim.epsilon(m) @@ -139,7 +146,7 @@ def test_monitors_data_size(): ), ], monitors=[ - td.FieldMonitor(size=(0, 0, 0), center=(0, 0, 0), freqs=[1, 2], name="point"), + td.FieldMonitor(size=(0, 0, 0), center=(0, 0, 0), freqs=[1e12, 2e12], name="point"), td.FluxTimeMonitor(size=(1, 1, 0), center=(0, 0, 0), interval=10, name="plane"), ], symmetry=(0, 1, -1), @@ -275,11 +282,11 @@ def _test_monitor_size(): def test_monitor_medium_frequency_range(log_capture, freq, log_level): # monitor frequency above or below a given medium's range should throw a warning - medium = td.Medium(frequency_range=(2, 3)) + medium = td.Medium(frequency_range=(2e12, 3e12)) box = td.Structure(geometry=td.Box(size=(0.1, 0.1, 0.1)), medium=medium) - mnt = td.FieldMonitor(size=(0, 0, 0), name="freq", freqs=[freq]) + mnt = td.FieldMonitor(size=(0, 0, 0), name="freq", freqs=[freq * 1e12]) src = td.UniformCurrentSource( - source_time=td.GaussianPulse(freq0=2.5, fwidth=0.5), + source_time=td.GaussianPulse(freq0=2.5e12, fwidth=0.5e12), size=(0, 0, 0), polarization="Ex", ) @@ -299,11 +306,11 @@ def test_monitor_simulation_frequency_range(log_capture, fwidth, log_level): # monitor frequency outside of the simulation's frequency range should throw a warning src = td.UniformCurrentSource( - source_time=td.GaussianPulse(freq0=2.0, fwidth=fwidth), + source_time=td.GaussianPulse(freq0=2.0e12, fwidth=fwidth), size=(0, 0, 0), polarization="Ex", ) - mnt = td.FieldMonitor(size=(0, 0, 0), name="freq", freqs=[1.5]) + mnt = td.FieldMonitor(size=(0, 0, 0), name="freq", freqs=[1.5e12]) _ = td.Simulation( size=(1, 1, 1), monitors=[mnt], @@ -331,12 +338,12 @@ def test_validate_bloch_with_symmetry(): def test_validate_normalize_index(): src = td.UniformCurrentSource( - source_time=td.GaussianPulse(freq0=2.0, fwidth=1.0), + source_time=td.GaussianPulse(freq0=2.0e12, fwidth=1.0e12), size=(0, 0, 0), polarization="Ex", ) src0 = td.UniformCurrentSource( - source_time=td.GaussianPulse(freq0=2.0, fwidth=1.0, amplitude=0), + source_time=td.GaussianPulse(freq0=2.0e12, fwidth=1.0e12, amplitude=0), size=(0, 0, 0), polarization="Ex", ) @@ -454,7 +461,7 @@ def test_validate_plane_wave_boundaries(log_capture): def test_validate_zero_dim_boundaries(log_capture): - # zero-dim simulation with an absorbing boundary in that direction should warn + # zero-dim simulation with an absorbing boundary in that direction should error src = td.PlaneWave( source_time=td.GaussianPulse(freq0=2.5e14, fwidth=1e13), center=(0, 0, 0), @@ -463,19 +470,19 @@ def test_validate_zero_dim_boundaries(log_capture): pol_angle=0.0, ) - td.Simulation( - size=(1, 1, 0), - run_time=1e-12, - sources=[src], - boundary_spec=td.BoundarySpec( - x=td.Boundary.periodic(), - y=td.Boundary.periodic(), - z=td.Boundary.pml(), - ), - ) - assert_log_level(log_capture, "WARNING") + with pytest.raises(pydantic.ValidationError): + td.Simulation( + size=(1, 1, 0), + run_time=1e-12, + sources=[src], + boundary_spec=td.BoundarySpec( + x=td.Boundary.periodic(), + y=td.Boundary.periodic(), + z=td.Boundary.pml(), + ), + ) - # zero-dim simulation with an absorbing boundary any other direction should not warn + # zero-dim simulation with an absorbing boundary any other direction should not error td.Simulation( size=(1, 1, 0), run_time=1e-12, @@ -483,7 +490,7 @@ def test_validate_zero_dim_boundaries(log_capture): boundary_spec=td.BoundarySpec( x=td.Boundary.pml(), y=td.Boundary.stable_pml(), - z=td.Boundary.pec(), + z=td.Boundary.periodic(), ), ) @@ -491,7 +498,6 @@ def test_validate_zero_dim_boundaries(log_capture): def test_validate_components_none(): assert SIM._structures_not_at_edges(val=None, values=SIM.dict()) is None - assert SIM._validate_num_mediums(val=None) is None assert SIM._validate_num_sources(val=None) is None assert SIM._warn_monitor_mediums_frequency_range(val=None, values=SIM.dict()) is None assert SIM._warn_monitor_simulation_frequency_range(val=None, values=SIM.dict()) is None @@ -524,20 +530,22 @@ def test_validate_mnt_size(monkeypatch, log_capture): # warning for monitor size monkeypatch.setattr(simulation, "WARN_MONITOR_DATA_SIZE_GB", 1 / 2**30) - s = SIM.copy(update=dict(monitors=(td.FieldMonitor(name="f", freqs=[1], size=(1, 1, 1)),))) + s = SIM.copy(update=dict(monitors=(td.FieldMonitor(name="f", freqs=[1e12], size=(1, 1, 1)),))) s._validate_monitor_size() assert_log_level(log_capture, "WARNING") # error for simulation size monkeypatch.setattr(simulation, "MAX_SIMULATION_DATA_SIZE_GB", 1 / 2**30) with pytest.raises(SetupError): - s = SIM.copy(update=dict(monitors=(td.FieldMonitor(name="f", freqs=[1], size=(1, 1, 1)),))) + s = SIM.copy( + update=dict(monitors=(td.FieldMonitor(name="f", freqs=[1e12], size=(1, 1, 1)),)) + ) s._validate_monitor_size() def test_max_geometry_validation(): gs = td.GridSpec(wavelength=1.0) - too_many = [td.Box(size=(1, 1, 1)) for _ in range(simulation.MAX_GEOMETRY_COUNT + 1)] + too_many = [td.Box(size=(1, 1, 1)) for _ in range(MAX_GEOMETRY_COUNT + 1)] fine = [ td.Structure( @@ -565,7 +573,7 @@ def test_max_geometry_validation(): medium=td.Medium(permittivity=2.0), ), ] - with pytest.raises(pydantic.ValidationError, match=f" {simulation.MAX_GEOMETRY_COUNT + 2} "): + with pytest.raises(pydantic.ValidationError, match=f" {MAX_GEOMETRY_COUNT + 2} "): _ = td.Simulation(size=(1, 1, 1), run_time=1, grid_spec=gs, structures=not_fine) @@ -575,13 +583,12 @@ def test_no_monitor(): def test_plot_structure(): - ax = SIM_FULL.structures[0].plot(x=0) + _ = SIM_FULL.structures[0].plot(x=0) plt.close() def test_plot_eps(): ax = SIM_FULL.plot_eps(x=0) - SIM_FULL._add_cbar(eps_min=1, eps_max=2, ax=ax) plt.close() @@ -606,6 +613,7 @@ def test_plot_1d_sim(): size=(0, 0, 1), grid_spec=grid_spec, run_time=1e-13, + boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), ) _ = s.plot(y=0) plt.close() @@ -724,26 +732,6 @@ def test_discretize_non_intersect(log_capture): assert_log_level(log_capture, "ERROR") -def test_filter_structures(): - s1 = td.Structure(geometry=td.Box(size=(1, 1, 1)), medium=SIM.medium) - s2 = td.Structure(geometry=td.Box(size=(1, 1, 1), center=(1, 1, 1)), medium=SIM.medium) - plane = td.Box(center=(0, 0, 1.5), size=(td.inf, td.inf, 0)) - SIM._filter_structures_plane(structures=[s1, s2], plane=plane) - - -def test_get_structure_plot_params(): - pp = SIM_FULL._get_structure_plot_params(mat_index=0, medium=SIM_FULL.medium) - assert pp.facecolor == "white" - pp = SIM_FULL._get_structure_plot_params(mat_index=1, medium=td.PEC) - assert pp.facecolor == "gold" - pp = SIM_FULL._get_structure_eps_plot_params( - medium=SIM_FULL.medium, freq=1, eps_min=1, eps_max=2 - ) - assert float(pp.facecolor) == 1.0 - pp = SIM_FULL._get_structure_eps_plot_params(medium=td.PEC, freq=1, eps_min=1, eps_max=2) - assert pp.facecolor == "gold" - - def test_warn_sim_background_medium_freq_range(log_capture): _ = SIM.copy( update=dict( @@ -752,8 +740,8 @@ def test_warn_sim_background_medium_freq_range(log_capture): polarization="Ex", source_time=td.GaussianPulse(freq0=2e14, fwidth=1e11) ), ), - monitors=(td.FluxMonitor(name="test", freqs=[2], size=(1, 1, 0)),), - medium=td.Medium(frequency_range=(0, 1)), + monitors=(td.FluxMonitor(name="test", freqs=[2e12], size=(1, 1, 0)),), + medium=td.Medium(frequency_range=(0, 1e12)), ) ) assert_log_level(log_capture, "WARNING") @@ -935,11 +923,18 @@ def test_sim_monitor_homogeneous(): boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), ) + # will be removed in 3.0 mediums = td.Simulation.intersecting_media(monitor_n2f_vol, [box]) assert len(mediums) == 1 mediums = td.Simulation.intersecting_media(monitor_n2f_vol, [box_transparent]) assert len(mediums) == 1 + # continue in 3.0 + mediums = td.Scene.intersecting_media(monitor_n2f_vol, [box]) + assert len(mediums) == 1 + mediums = td.Scene.intersecting_media(monitor_n2f_vol, [box_transparent]) + assert len(mediums) == 1 + # when another medium intersects an excluded surface, no errors should be raised monitor_n2f_vol_exclude = td.FieldProjectionAngleMonitor( center=(0.2, 0, 0.2), @@ -1040,6 +1035,167 @@ def test_proj_monitor_distance(log_capture): ) +def test_proj_monitor_warnings(log_capture): + """Test the validator that warns if projecting backwards.""" + + src = td.PlaneWave( + source_time=td.GaussianPulse(freq0=2.5e14, fwidth=1e13), + center=(0, 0, -0.5), + size=(td.inf, td.inf, 0), + direction="+", + pol_angle=-1.0, + ) + + # Cartesian monitor projecting backwards + monitor_n2f = td.FieldProjectionCartesianMonitor( + center=(0, 0, 0), + size=(td.inf, td.inf, 0), + freqs=[2.5e14], + name="monitor_n2f", + x=[4], + y=[5], + proj_distance=-1e5, + proj_axis=2, + ) + _ = td.Simulation( + size=(1, 1, 1), + structures=[], + sources=[src], + run_time=1e-12, + monitors=[monitor_n2f], + ) + assert_log_level(log_capture, "WARNING") + log_capture.clear() + + # Cartesian monitor with custom origin projecting backwards + monitor_n2f = td.FieldProjectionCartesianMonitor( + center=(0, 0, 0), + size=(td.inf, td.inf, 0), + freqs=[2.5e14], + name="monitor_n2f", + x=[4], + y=[5], + proj_distance=39, + proj_axis=2, + custom_origin=(1, 2, -40), + ) + _ = td.Simulation( + size=(1, 1, 1), + structures=[], + sources=[src], + run_time=1e-12, + monitors=[monitor_n2f], + ) + assert_log_level(log_capture, "WARNING") + log_capture.clear() + + # Cartesian monitor with custom origin projecting backwards with normal_dir '-' + monitor_n2f = td.FieldProjectionCartesianMonitor( + center=(0, 0, 0), + size=(td.inf, td.inf, 0), + freqs=[2.5e14], + name="monitor_n2f", + x=[4], + y=[5], + proj_distance=41, + proj_axis=2, + custom_origin=(1, 2, -40), + normal_dir="-", + ) + _ = td.Simulation( + size=(1, 1, 1), + structures=[], + sources=[src], + run_time=1e-12, + monitors=[monitor_n2f], + ) + assert_log_level(log_capture, "WARNING") + log_capture.clear() + + # Angle monitor projecting backwards + monitor_n2f = td.FieldProjectionAngleMonitor( + center=(0, 0, 0), + size=(td.inf, td.inf, 0), + freqs=[2.5e14], + name="monitor_n2f", + theta=[np.pi / 2 + 1e-2], + phi=[0], + proj_distance=1e3, + ) + _ = td.Simulation( + size=(1, 1, 1), + structures=[], + sources=[src], + run_time=1e-12, + monitors=[monitor_n2f], + ) + assert_log_level(log_capture, "WARNING") + log_capture.clear() + + # Angle monitor projecting backwards with custom origin + monitor_n2f = td.FieldProjectionAngleMonitor( + center=(0, 0, 0), + size=(td.inf, td.inf, 0), + freqs=[2.5e14], + name="monitor_n2f", + theta=[np.pi / 2 - 0.02], + phi=[0], + proj_distance=10, + custom_origin=(0, 0, -0.5), + ) + _ = td.Simulation( + size=(1, 1, 1), + structures=[], + sources=[src], + run_time=1e-12, + monitors=[monitor_n2f], + ) + assert_log_level(log_capture, "WARNING") + log_capture.clear() + + # Angle monitor projecting backwards with custom origin and normal_dir '-' + monitor_n2f = td.FieldProjectionAngleMonitor( + center=(0, 0, 0), + size=(td.inf, td.inf, 0), + freqs=[2.5e14], + name="monitor_n2f", + theta=[np.pi / 2 + 0.02], + phi=[0], + proj_distance=10, + custom_origin=(0, 0, 0.5), + normal_dir="-", + ) + _ = td.Simulation( + size=(1, 1, 1), + structures=[], + sources=[src], + run_time=1e-12, + monitors=[monitor_n2f], + ) + assert_log_level(log_capture, "WARNING") + log_capture.clear() + + # Cartesian monitor using approximations but too short proj_distance + monitor_n2f = td.FieldProjectionCartesianMonitor( + center=(0, 0, 0), + size=(td.inf, td.inf, 0), + freqs=[2.5e14], + name="monitor_n2f", + x=[4], + y=[5], + proj_distance=9, + proj_axis=2, + ) + _ = td.Simulation( + size=(1, 1, 1), + structures=[], + sources=[src], + run_time=1e-12, + monitors=[monitor_n2f], + ) + assert_log_level(log_capture, "WARNING") + + def test_diffraction_medium(): """Make sure we error if a diffraction monitor is in a lossy medium.""" @@ -1240,9 +1396,9 @@ def _test_names_default(): ), ], monitors=[ - td.FluxMonitor(size=(1, 1, 0), center=(0, -0.5, 0), freqs=[1], name="mon1"), - td.FluxMonitor(size=(0, 1, 1), center=(0, -0.5, 0), freqs=[1], name="mon2"), - td.FluxMonitor(size=(1, 0, 1), center=(0, -0.5, 0), freqs=[1], name="mon3"), + td.FluxMonitor(size=(1, 1, 0), center=(0, -0.5, 0), freqs=[1e12], name="mon1"), + td.FluxMonitor(size=(0, 1, 1), center=(0, -0.5, 0), freqs=[1e12], name="mon2"), + td.FluxMonitor(size=(1, 0, 1), center=(0, -0.5, 0), freqs=[1e12], name="mon3"), ], boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), ) @@ -1303,8 +1459,8 @@ def test_names_unique(): size=(2.0, 2.0, 2.0), run_time=1e-12, monitors=[ - td.FluxMonitor(size=(1, 1, 0), center=(0, -0.5, 0), freqs=[1], name="mon1"), - td.FluxMonitor(size=(0, 1, 1), center=(0, -0.5, 0), freqs=[1], name="mon1"), + td.FluxMonitor(size=(1, 1, 0), center=(0, -0.5, 0), freqs=[1e12], name="mon1"), + td.FluxMonitor(size=(0, 1, 1), center=(0, -0.5, 0), freqs=[1e12], name="mon1"), ], boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), ) @@ -1312,7 +1468,7 @@ def test_names_unique(): def test_mode_object_syms(): """Test that errors are raised if a mode object is not placed right in the presence of syms.""" - g = td.GaussianPulse(freq0=1, fwidth=0.1) + g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) # wrong mode source with pytest.raises(pydantic.ValidationError): @@ -1335,7 +1491,7 @@ def test_mode_object_syms(): run_time=1e-12, symmetry=(1, -1, 0), monitors=[ - td.ModeMonitor(size=(2, 2, 0), name="mnt", freqs=[2], mode_spec=td.ModeSpec()) + td.ModeMonitor(size=(2, 2, 0), name="mnt", freqs=[2e12], mode_spec=td.ModeSpec()) ], boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), ) @@ -1360,7 +1516,7 @@ def test_mode_object_syms(): symmetry=(1, -1, 0), monitors=[ td.ModeMonitor( - center=(2, 0, 1), size=(2, 2, 0), name="mnt", freqs=[2], mode_spec=td.ModeSpec() + center=(2, 0, 1), size=(2, 2, 0), name="mnt", freqs=[2e12], mode_spec=td.ModeSpec() ) ], boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), @@ -1369,7 +1525,7 @@ def test_mode_object_syms(): def test_tfsf_symmetry(): """Test that a TFSF source cannot be set in the presence of symmetries.""" - src_time = td.GaussianPulse(freq0=1, fwidth=0.1) + src_time = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) source = td.TFSF( size=[1, 1, 1], @@ -1393,7 +1549,7 @@ def test_tfsf_symmetry(): def test_tfsf_boundaries(log_capture): """Test that a TFSF source is allowed to cross boundaries only in particular cases.""" - src_time = td.GaussianPulse(freq0=td.C_0, fwidth=0.1) + src_time = td.GaussianPulse(freq0=td.C_0, fwidth=0.1e12) source = td.TFSF( size=[1, 1, 1], @@ -1476,7 +1632,7 @@ def test_tfsf_boundaries(log_capture): def test_tfsf_structures_grid(log_capture): """Test that a TFSF source is allowed to intersect structures only in particular cases.""" - src_time = td.GaussianPulse(freq0=td.C_0, fwidth=0.1) + src_time = td.GaussianPulse(freq0=td.C_0, fwidth=0.1e12) source = td.TFSF( size=[1, 1, 1], @@ -1602,7 +1758,7 @@ def test_warn_large_epsilon(log_capture, size, num_struct, log_level): center=(0, 0, 0), size=(td.inf, td.inf, 0), direction="+", - source_time=td.GaussianPulse(freq0=1, fwidth=0.1), + source_time=td.GaussianPulse(freq0=1e12, fwidth=0.1e12), ) ], structures=structures, @@ -1613,7 +1769,7 @@ def test_warn_large_epsilon(log_capture, size, num_struct, log_level): @pytest.mark.parametrize("dl, log_level", [(0.1, None), (0.005, "WARNING")]) def test_warn_large_mode_monitor(log_capture, dl, log_level): - """Make sure we get a warning if the epsilon grid is too large.""" + """Make sure we get a warning if the mode monitor grid is too large.""" sim = td.Simulation( size=(2.0, 2.0, 2.0), @@ -1623,12 +1779,12 @@ def test_warn_large_mode_monitor(log_capture, dl, log_level): td.ModeSource( size=(0.1, 0.1, 0), direction="+", - source_time=td.GaussianPulse(freq0=1, fwidth=0.1), + source_time=td.GaussianPulse(freq0=1e12, fwidth=0.1e12), ) ], monitors=[ td.ModeMonitor( - size=(td.inf, 0, td.inf), freqs=[1], name="test", mode_spec=td.ModeSpec() + size=(td.inf, 0, td.inf), freqs=[1e12], name="test", mode_spec=td.ModeSpec() ) ], ) @@ -1638,7 +1794,7 @@ def test_warn_large_mode_monitor(log_capture, dl, log_level): @pytest.mark.parametrize("dl, log_level", [(0.1, None), (0.005, "WARNING")]) def test_warn_large_mode_source(log_capture, dl, log_level): - """Make sure we get a warning if the epsilon grid is too large.""" + """Make sure we get a warning if the mode source grid is too large.""" sim = td.Simulation( size=(2.0, 2.0, 2.0), @@ -1648,7 +1804,7 @@ def test_warn_large_mode_source(log_capture, dl, log_level): td.ModeSource( size=(td.inf, td.inf, 0), direction="+", - source_time=td.GaussianPulse(freq0=1, fwidth=0.1), + source_time=td.GaussianPulse(freq0=1e12, fwidth=0.1e12), ) ], ) @@ -1656,6 +1812,33 @@ def test_warn_large_mode_source(log_capture, dl, log_level): assert_log_level(log_capture, log_level) +def test_error_large_monitors(): + """Test if various large monitors cause pre-upload validation to error.""" + + sim = td.Simulation( + size=(2.0, 2.0, 2.0), + grid_spec=td.GridSpec.uniform(dl=0.005), + run_time=1e-12, + boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), + ) + mnt_size = (td.inf, 0, td.inf) + mnt_test = [ + td.ModeMonitor(size=mnt_size, freqs=[1e12], name="test", mode_spec=td.ModeSpec()), + td.ModeSolverMonitor(size=mnt_size, freqs=[1e12], name="test", mode_spec=td.ModeSpec()), + td.FluxMonitor(size=mnt_size, freqs=[1e12], name="test"), + td.FluxTimeMonitor(size=mnt_size, name="test"), + td.DiffractionMonitor(size=mnt_size, freqs=[1e12], name="test"), + td.FieldProjectionAngleMonitor( + size=mnt_size, freqs=[1e12], name="test", theta=[0], phi=[0] + ), + ] + + for monitor in mnt_test: + with pytest.raises(SetupError): + s = sim.updated_copy(monitors=[monitor]) + s.validate_pre_upload() + + def test_dt(): """make sure dt is reduced when there is a medium with eps_inf < 1.""" sim = td.Simulation( @@ -1719,7 +1902,7 @@ def test_sim_volumetric_structures(log_capture, tmp_path): ) box = td.Structure( geometry=td.Box(size=(td.inf, td.inf, 0)), - medium=td.Medium2D.from_medium(td.PEC, thickness=thickness), + medium=td.Medium2D.from_medium(td.Medium(permittivity=1), thickness=thickness), ) below = td.Structure( geometry=td.Box.from_bounds([-td.inf, -td.inf, -1000], [td.inf, td.inf, 0]), @@ -1750,11 +1933,26 @@ def test_sim_volumetric_structures(log_capture, tmp_path): rtol=RTOL, ) assert np.isclose(sim.volumetric_structures[1].medium.yy.to_medium().permittivity, 1, rtol=RTOL) - assert np.isclose( - sim.volumetric_structures[1].medium.xx.to_medium().conductivity, - LARGE_NUMBER * thickness / grid_dl, - rtol=RTOL, + + # PEC + box = td.Structure( + geometry=td.Box(size=(td.inf, td.inf, 0)), + medium=td.PEC2D, + ) + sim = td.Simulation( + size=(10, 10, 10), + structures=[below, box], + sources=[src], + monitors=[monitor], + boundary_spec=td.BoundarySpec( + x=td.Boundary.pml(num_layers=5), + y=td.Boundary.pml(num_layers=5), + z=td.Boundary.pml(num_layers=5), + ), + grid_spec=td.GridSpec.uniform(dl=grid_dl), + run_time=1e-12, ) + assert isinstance(sim.volumetric_structures[1].medium.xx, td.PECMedium) log_capture.clear() @@ -1979,3 +2177,83 @@ def test_perturbed_mediums_copy(): assert isinstance(new_sim.medium, td.CustomMedium) assert isinstance(new_sim.structures[0].medium, td.CustomPoleResidue) + + +def test_scene_from_scene(): + """Test .scene and .from_scene functionality.""" + + scene = SIM_FULL.scene + + sim = td.Simulation.from_scene( + scene=scene, + **SIM_FULL.dict(exclude={"structures", "medium"}), + ) + + assert sim == SIM_FULL + + +def test_to_gds(tmp_path): + sim = td.Simulation( + size=(2.0, 2.0, 2.0), + run_time=1e-12, + structures=[ + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), + medium=td.Medium(permittivity=2.0), + ), + td.Structure( + geometry=td.Sphere(radius=1.4, center=(1.0, 0.0, 1.0)), + medium=td.Medium(permittivity=1.5), + ), + td.Structure( + geometry=td.Cylinder(radius=1.4, length=2.0, center=(1.0, 0.0, -1.0), axis=1), + medium=td.Medium(), + ), + ], + sources=[ + td.PointDipole( + center=(0, 0, 0), + polarization="Ex", + source_time=td.GaussianPulse(freq0=1e14, fwidth=1e12), + ) + ], + monitors=[ + td.FieldMonitor(size=(0, 0, 0), center=(0, 0, 0), freqs=[1e12, 2e12], name="point"), + ], + boundary_spec=td.BoundarySpec( + x=td.Boundary.pml(num_layers=20), + y=td.Boundary.stable_pml(num_layers=30), + z=td.Boundary.absorber(num_layers=100), + ), + shutoff=1e-6, + ) + + fname = str(tmp_path / "simulation_z.gds") + sim.to_gds_file( + fname, z=0, gds_layer_dtype_map={td.Medium(permittivity=2.0): (2, 1), td.Medium(): (1, 0)} + ) + cell = gdstk.read_gds(fname).cells[0] + assert cell.name == "MAIN" + assert len(cell.polygons) >= 3 + areas = cell.area(True) + assert (2, 1) in areas + assert (1, 0) in areas + assert (0, 0) in areas + assert np.allclose(areas[(2, 1)], 0.5) + assert np.allclose(areas[(1, 0)], 2.0 * (1.4**2 - 1) ** 0.5, atol=1e-2) + assert np.allclose(areas[(0, 0)], 0.5 * np.pi * (1.4**2 - 1), atol=1e-2) + + fname = str(tmp_path / "simulation_y.gds") + sim.to_gds_file( + fname, y=0, gds_layer_dtype_map={td.Medium(permittivity=2.0): (2, 1), td.Medium(): (1, 0)} + ) + cell = gdstk.read_gds(fname).cells[0] + assert cell.name == "MAIN" + assert len(cell.polygons) >= 3 + areas = cell.area(True) + assert (2, 1) in areas + assert (1, 0) in areas + assert (0, 0) in areas + assert np.allclose(areas[(2, 1)], 0.5) + assert np.allclose(areas[(1, 0)], 0.25 * np.pi * 1.4**2, atol=1e-2) + assert np.allclose(areas[(0, 0)], 0.25 * np.pi * 1.4**2, atol=1e-2) diff --git a/tests/test_components/test_source.py b/tests/test_components/test_source.py index c59a5868a..ac4a7a95e 100644 --- a/tests/test_components/test_source.py +++ b/tests/test_components/test_source.py @@ -6,6 +6,7 @@ import tidy3d as td from tidy3d.exceptions import SetupError from tidy3d.components.source import DirectionalSource, CHEB_GRID_WIDTH +from ..utils import assert_log_level, log_capture ST = td.GaussianPulse(freq0=2e14, fwidth=1e14) S = td.PointDipole(source_time=ST, polarization="Ex") @@ -46,7 +47,7 @@ def test_dir_vector(): def test_UniformCurrentSource(): - g = td.GaussianPulse(freq0=1, fwidth=0.1) + g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) # test we can make generic UniformCurrentSource _ = td.UniformCurrentSource(size=(1, 1, 1), source_time=g, polarization="Ez", interpolate=False) @@ -56,8 +57,8 @@ def test_UniformCurrentSource(): def test_source_times(): # test we can make gaussian pulse - g = td.GaussianPulse(freq0=1, fwidth=0.1) - ts = np.linspace(0, 30, 1001) + g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) + ts = np.linspace(0, 30, 1001) * 1e-12 g.amp_time(ts) # g.plot(ts) # plt.close() @@ -65,23 +66,21 @@ def test_source_times(): # test we can make cw pulse from tidy3d.components.source import ContinuousWave - c = ContinuousWave(freq0=1, fwidth=0.1) - ts = np.linspace(0, 30, 1001) + c = ContinuousWave(freq0=1e12, fwidth=0.1e12) c.amp_time(ts) # test gaussian pulse with and without DC component - g = td.GaussianPulse(freq0=0.1, fwidth=1) - ts = np.linspace(0, 30, 1001) + g = td.GaussianPulse(freq0=0.1e12, fwidth=1e12) dc_comp = g.spectrum(ts, [0], ts[1] - ts[0]) - assert abs(dc_comp) ** 2 < ATOL - g = td.GaussianPulse(freq0=0.1, fwidth=1, remove_dc_component=False) + assert abs(dc_comp) ** 2 < 1e-32 + g = td.GaussianPulse(freq0=0.1e12, fwidth=1e12, remove_dc_component=False) dc_comp = g.spectrum(ts, [0], ts[1] - ts[0]) - assert abs(dc_comp) ** 2 > ATOL + assert abs(dc_comp) ** 2 > 1e-32 def test_dipole(): - g = td.GaussianPulse(freq0=1, fwidth=0.1) + g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) _ = td.PointDipole(center=(1, 2, 3), source_time=g, polarization="Ex", interpolate=True) _ = td.PointDipole(center=(1, 2, 3), source_time=g, polarization="Ex", interpolate=False) # p.plot(y=2) @@ -92,7 +91,7 @@ def test_dipole(): def test_FieldSource(): - g = td.GaussianPulse(freq0=1, fwidth=0.1) + g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) mode_spec = td.ModeSpec(num_modes=2) # test we can make planewave @@ -152,7 +151,7 @@ def test_FieldSource(): def test_pol_arrow(): - g = td.GaussianPulse(freq0=1, fwidth=0.1) + g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) def get_pol_dir(axis, pol_angle=0, angle_theta=0, angle_phi=0): @@ -191,7 +190,7 @@ def get_pol_dir(axis, pol_angle=0, angle_theta=0, angle_phi=0): def test_broadband_source(): - g = td.GaussianPulse(freq0=1, fwidth=0.1) + g = td.GaussianPulse(freq0=1e12, fwidth=0.1e12) mode_spec = td.ModeSpec(num_modes=2) fmin, fmax = g.frequency_range(num_fwidth=CHEB_GRID_WIDTH) fdiff = (fmax - fmin) / 2 @@ -268,43 +267,64 @@ def check_freq_grid(freq_grid, num_freqs): ) -def test_custom_source_time(): - ts = np.linspace(0, 30, 1001) +def test_custom_source_time(log_capture): + ts = np.linspace(0, 30e-12, 1001) amp_time = ts / max(ts) + freq0 = 1e12 # basic test - cst = td.CustomSourceTime.from_values(freq0=1, fwidth=0.1, values=amp_time, dt=ts[1] - ts[0]) - assert np.allclose(cst.amp_time(ts), amp_time * np.exp(-1j * 2 * np.pi * ts), rtol=0, atol=ATOL) - - # test single value validation error - with pytest.raises(pydantic.ValidationError): - vals = td.components.data.data_array.TimeDataArray([1], coords=dict(t=[0])) - dataset = td.components.data.dataset.TimeDataset(values=vals) - cst = td.CustomSourceTime(source_time_dataset=dataset, freq0=1, fwidth=0.1) - assert np.allclose(cst.amp_time([0]), [1], rtol=0, atol=ATOL) + cst = td.CustomSourceTime.from_values( + freq0=freq0, fwidth=0.1e12, values=amp_time, dt=ts[1] - ts[0] + ) + assert np.allclose( + cst.amp_time(ts), amp_time * np.exp(-1j * 2 * np.pi * ts * freq0), rtol=0, atol=ATOL + ) # test interpolation - cst = td.CustomSourceTime.from_values(freq0=1, fwidth=0.1, values=np.linspace(0, 9, 10), dt=0.1) + cst = td.CustomSourceTime.from_values( + freq0=freq0, fwidth=0.1e12, values=np.linspace(0, 9, 10), dt=0.1e-12 + ) assert np.allclose( - cst.amp_time(0.09), [0.9 * np.exp(-1j * 2 * np.pi * 0.09)], rtol=0, atol=ATOL + cst.amp_time(0.09e-12), + [0.9 * np.exp(-1j * 2 * np.pi * 0.09e-12 * freq0)], + rtol=0, + atol=ATOL, ) - # test sampling warning - cst = td.CustomSourceTime.from_values(freq0=1, fwidth=0.1, values=np.linspace(0, 9, 10), dt=0.1) + # test out of range handling source = td.PointDipole(center=(0, 0, 0), source_time=cst, polarization="Ex") + monitor = td.FieldMonitor(size=(td.inf, td.inf, 0), freqs=[1e12], name="field") sim = td.Simulation( size=(10, 10, 10), run_time=1e-12, grid_spec=td.GridSpec.uniform(dl=0.1), sources=[source], + normalize_index=None, ) - - # test out of range handling - vals = [1] - cst = td.CustomSourceTime.from_values(freq0=1, fwidth=0.1, values=[0, 1], dt=sim.dt) + cst = td.CustomSourceTime.from_values(freq0=freq0, fwidth=0.1e12, values=[0, 1], dt=sim.dt) source = td.PointDipole(center=(0, 0, 0), source_time=cst, polarization="Ex") sim = sim.updated_copy(sources=[source]) assert np.allclose(cst.amp_time(sim.tmesh[0]), [0], rtol=0, atol=ATOL) assert np.allclose( - cst.amp_time(sim.tmesh[1:]), np.exp(-1j * 2 * np.pi * sim.tmesh[1:]), rtol=0, atol=ATOL + cst.amp_time(sim.tmesh[1:]), + np.exp(-1j * 2 * np.pi * sim.tmesh[1:] * freq0), + rtol=0, + atol=ATOL, ) + + assert_log_level(log_capture, None) + + # test normalization warning + sim = sim.updated_copy(normalize_index=0) + assert_log_level(log_capture, "WARNING") + log_capture.clear() + source = source.updated_copy(source_time=td.ContinuousWave(freq0=freq0, fwidth=0.1e12)) + sim = sim.updated_copy(sources=[source]) + assert_log_level(log_capture, "WARNING") + + # test single value validation error + with pytest.raises(pydantic.ValidationError): + vals = td.components.data.data_array.TimeDataArray([1], coords=dict(t=[0])) + dataset = td.components.data.dataset.TimeDataset(values=vals) + cst = td.CustomSourceTime(source_time_dataset=dataset, freq0=freq0, fwidth=0.1e12) + assert np.allclose(cst.amp_time([0]), [1], rtol=0, atol=ATOL) diff --git a/tests/test_components/test_structure.py b/tests/test_components/test_structure.py new file mode 100644 index 000000000..45f6e2a73 --- /dev/null +++ b/tests/test_components/test_structure.py @@ -0,0 +1,137 @@ +import pytest +import gdstk +import numpy as np +import tidy3d as td +import pydantic.v1 as pd + + +def test_to_gds(tmp_path): + geometry = td.Box(size=(2, 2, 2)) + medium = td.Medium() + structure = td.Structure(geometry=geometry, medium=medium) + + fname = str(tmp_path / "structure.gds") + structure.to_gds_file(fname, x=0, gds_cell_name="X") + cell = gdstk.read_gds(fname).cells[0] + assert cell.name == "X" + assert len(cell.polygons) == 1 + assert np.allclose(cell.polygons[0].area(), 4.0) + + +def test_custom_medium_to_gds(tmp_path): + geometry = td.Box(size=(2, 2, 2)) + + nx, ny, nz = 100, 90, 80 + x = np.linspace(0, 2, nx) + y = np.linspace(-1, 1, ny) + z = np.linspace(-1, 1, nz) + f = np.array([td.C_0]) + mx, my, mz, _ = np.meshgrid(x, y, z, f, indexing="ij", sparse=True) + data = 1 + 1 / (1 + (mx - 1) ** 2 + my**2 + mz**2) + eps_diagonal_data = td.ScalarFieldDataArray(data, coords=dict(x=x, y=y, z=z, f=f)) + eps_components = {f"eps_{d}{d}": eps_diagonal_data for d in "xyz"} + eps_dataset = td.PermittivityDataset(**eps_components) + medium = td.CustomMedium(eps_dataset=eps_dataset, name="my_medium") + structure = td.Structure(geometry=geometry, medium=medium) + + fname = str(tmp_path / "structure-custom-x.gds") + structure.to_gds_file(fname, x=1, permittivity_threshold=1.5, frequency=td.C_0) + cell = gdstk.read_gds(fname).cells[0] + assert np.allclose(cell.area(), np.pi, atol=1e-2) + + fname = str(tmp_path / "structure-custom-z.gds") + structure.to_gds_file(fname, z=0, permittivity_threshold=1.5, frequency=td.C_0) + cell = gdstk.read_gds(fname).cells[0] + assert np.allclose(cell.area(), np.pi / 2, atol=3e-2) + + fname = str(tmp_path / "structure-empty.gds") + structure.to_gds_file(fname, x=-0.1, permittivity_threshold=1.5, frequency=td.C_0) + cell = gdstk.read_gds(fname).cells[0] + assert len(cell.polygons) == 0 + + +def test_lower_dimension_custom_medium_to_gds(tmp_path): + geometry = td.Box(size=(2, 0, 2)) + + nx, nz = 100, 80 + x = np.linspace(0, 2, nx) + y = np.array([0.0]) + z = np.linspace(-1, 1, nz) + f = np.array([td.C_0]) + mx, my, mz, _ = np.meshgrid(x, y, z, f, indexing="ij", sparse=True) + data = 1 + 1 / (1 + (mx - 1) ** 2 + mz**2) + eps_diagonal_data = td.ScalarFieldDataArray(data, coords=dict(x=x, y=y, z=z, f=f)) + eps_components = {f"eps_{d}{d}": eps_diagonal_data for d in "xyz"} + eps_dataset = td.PermittivityDataset(**eps_components) + medium = td.CustomMedium(eps_dataset=eps_dataset, name="my_medium") + structure = td.Structure(geometry=geometry, medium=medium) + + fname = str(tmp_path / "structure-custom-y.gds") + structure.to_gds_file(fname, y=0, permittivity_threshold=1.5, frequency=td.C_0) + cell = gdstk.read_gds(fname).cells[0] + assert np.allclose(cell.area(), np.pi / 2, atol=3e-2) + + +def test_non_symmetric_custom_medium_to_gds(tmp_path): + geometry = td.Box(size=(1, 2, 1), center=(0.5, 0, 2.5)) + + nx, ny, nz = 150, 80, 180 + x = np.linspace(0, 2, nx) + y = np.linspace(-1, 1, ny) + z = np.linspace(2, 3, nz) + f = np.array([td.C_0]) + mx, my, mz, _ = np.meshgrid(x, y, z, f, indexing="ij", sparse=True) + data = 1 + mx + 0 * my + (mz - 2) ** 2 + print(data.min(), data.max()) + + eps_diagonal_data = td.ScalarFieldDataArray(data, coords=dict(x=x, y=y, z=z, f=f)) + eps_components = {f"eps_{d}{d}": eps_diagonal_data for d in "xyz"} + eps_dataset = td.PermittivityDataset(**eps_components) + medium = td.CustomMedium(eps_dataset=eps_dataset, name="my_medium") + structure = td.Structure(geometry=geometry, medium=medium) + + fname = str(tmp_path / "structure-non-symmetric.gds") + structure.to_gds_file(fname, y=0, permittivity_threshold=2.0, frequency=td.C_0) + cell = gdstk.read_gds(fname).cells[0] + assert np.allclose(cell.bounding_box(), ((0, 2), (1, 3)), atol=0.1) + assert gdstk.inside([(0.1, 2.1), (0.5, 2.5), (0.9, 2.9)], cell.polygons) == (False, False, True) + + +@pytest.mark.parametrize("axis", [0, 1, 2]) +def test_invalid_polyslab(axis): + medium = td.Medium() + vertices = [(-1, -2), (-1, 1), (1, 2), (2, 1), (0, 1), (0, 0), (1.5, -0.5), (0, -1), (0, -2)] + i = (axis + 1) % 3 + j = (axis + 2) % 3 + + ps = td.PolySlab(vertices=vertices, slab_bounds=(-1, 1), sidewall_angle=0.1, axis=axis) + box = td.Box(size=(1, 1, 1)) + + geo0 = ps.rotated(-np.pi / 3, axis) + _ = td.Structure(geometry=geo0, medium=medium) + + geo1 = ps.rotated(-np.pi / 3, i).scaled(2, 2, 2).translated(-1, 0.5, 2).rotated(np.pi / 3, i) + _ = td.Structure(geometry=geo1, medium=medium) + + geo2 = ps.rotated(np.pi / 4, i) + with pytest.raises(pd.ValidationError): + _ = td.Structure(geometry=geo2, medium=medium) + + geo3 = ps.rotated(np.pi / 5, j) + with pytest.raises(pd.ValidationError): + _ = td.Structure(geometry=geo3, medium=medium) + + geo4 = ps.rotated(np.pi / 6, (1, 1, 1)) + with pytest.raises(pd.ValidationError): + _ = td.Structure(geometry=geo4, medium=medium) + + geo5 = td.GeometryGroup(geometries=[ps]).rotated(np.pi / 2, j) + with pytest.raises(pd.ValidationError): + _ = td.Structure(geometry=geo5, medium=medium) + + geo6 = td.GeometryGroup(geometries=[ps - box]).rotated(np.pi / 2, i) + with pytest.raises(pd.ValidationError): + _ = td.Structure(geometry=geo6, medium=medium) + + geo7 = td.GeometryGroup(geometries=[(ps - box).rotated(np.pi / 4, j)]).rotated(-np.pi / 4, j) + _ = td.Structure(geometry=geo7, medium=medium) diff --git a/tests/test_components/test_time_modulation.py b/tests/test_components/test_time_modulation.py new file mode 100644 index 000000000..f7fcfd44a --- /dev/null +++ b/tests/test_components/test_time_modulation.py @@ -0,0 +1,205 @@ +"""Tests space time modulation.""" +import numpy as np +import pytest +from math import isclose +import pydantic.v1 as pydantic +import tidy3d as td +from tidy3d.exceptions import ValidationError + +np.random.seed(4) + +# space +NX, NY, NZ = 10, 9, 8 +X = np.linspace(-1, 1, NX) +Y = np.linspace(-1, 1, NY) +Z = np.linspace(-1, 1, NZ) +COORDS = dict(x=X, y=Y, z=Z) +ARRAY_CMP = td.SpatialDataArray(np.random.random((NX, NY, NZ)) + 0.1j, coords=COORDS) +ARRAY = td.SpatialDataArray(np.random.random((NX, NY, NZ)), coords=COORDS) + +SP_UNIFORM = td.SpaceModulation() + +# time +FREQ_MODULATE = 1e12 +AMP_TIME = 1.1 +PHASE_TIME = 0 +CW = td.ContinuousWaveTimeModulation(freq0=FREQ_MODULATE, amplitude=AMP_TIME, phase=PHASE_TIME) + +# combined +ST = td.SpaceTimeModulation( + time_modulation=CW, +) + +# medium modulation spec +MODULATION_SPEC = td.ModulationSpec() + + +def test_time_modulation(): + """time modulation: only supporting CW for now.""" + assert isclose(np.real(CW.amp_time(1 / FREQ_MODULATE)), AMP_TIME) + assert isclose(CW.max_modulation, AMP_TIME) + + cw = CW.updated_copy(phase=np.pi / 4, amplitude=10) + assert isclose(np.real(cw.amp_time(1 / FREQ_MODULATE)), np.sqrt(2) / 2 * 10) + assert isclose(cw.max_modulation, 10) + + +def test_space_modulation(): + """uniform or custom space modulation""" + # uniform in both amplitude and phase + assert isclose(SP_UNIFORM.max_modulation, 1) + + # uniform in phase, but custom in amplitude + with pytest.raises(pydantic.ValidationError): + sp = SP_UNIFORM.updated_copy(amplitude=ARRAY_CMP) + + sp = SP_UNIFORM.updated_copy(amplitude=ARRAY) + assert isclose(sp.max_modulation, np.max(ARRAY)) + + # uniform in amplitude, but custom in phase + with pytest.raises(pydantic.ValidationError): + sp = SP_UNIFORM.updated_copy(phase=ARRAY_CMP) + sp = SP_UNIFORM.updated_copy(phase=ARRAY) + assert isclose(sp.max_modulation, 1) + + # custom in both + with pytest.raises(pydantic.ValidationError): + sp = SP_UNIFORM.updated_copy(phase=ARRAY_CMP, amplitude=ARRAY_CMP) + sp = SP_UNIFORM.updated_copy(phase=ARRAY, amplitude=ARRAY) + + +def test_space_time_modulation(): + # cw modulation, uniform in space + assert isclose(ST.max_modulation, AMP_TIME) + assert not ST.negligible_modulation + + # cw modulation, but 0 amplitude + st = ST.updated_copy(time_modulation=CW.updated_copy(amplitude=0)) + assert st.negligible_modulation + + st = ST.updated_copy(space_modulation=td.SpaceModulation(amplitude=0)) + assert st.negligible_modulation + + # cw modulation, nonuniform in space + st = ST.updated_copy(space_modulation=td.SpaceModulation(amplitude=ARRAY, phase=ARRAY)) + assert not st.negligible_modulation + assert isclose(st.max_modulation, AMP_TIME * np.max(ARRAY)) + + +def test_modulated_medium(): + """time modulated medium""" + # unmodulated + medium = td.Medium() + assert medium.modulation_spec is None + assert medium.time_modulated == False + + assert MODULATION_SPEC.applied_modulation == False + medium = medium.updated_copy(modulation_spec=MODULATION_SPEC) + assert medium.time_modulated == False + + # permittivity modulated + modulation_spec = MODULATION_SPEC.updated_copy(permittivity=ST) + # modulated permitivity <= 0 + with pytest.raises(pydantic.ValidationError): + medium = td.Medium(modulation_spec=modulation_spec) + medium = td.Medium(permittivity=2, modulation_spec=modulation_spec) + assert isclose(medium.n_cfl, np.sqrt(2 - AMP_TIME)) + + # conductivity modulated + modulation_spec = MODULATION_SPEC.updated_copy(conductivity=ST) + # modulated conductivity <= 0 + with pytest.raises(pydantic.ValidationError): + medium = td.Medium(modulation_spec=modulation_spec) + medium_sometimes_active = td.Medium(modulation_spec=modulation_spec, allow_gain=True) + medium = td.Medium(conductivity=2, modulation_spec=modulation_spec) + + # both modulated, but different time modulation: error + st_freq2 = ST.updated_copy( + time_modulation=td.ContinuousWaveTimeModulation(freq0=2e12, amplitude=2) + ) + with pytest.raises(pydantic.ValidationError): + modulation_spec = MODULATION_SPEC.updated_copy(permittivity=ST, conductivity=st_freq2) + # both modulated, but different space modulation: fine + st_space2 = ST.updated_copy(space_modulation=td.SpaceModulation(amplitude=0.1)) + modulation_spec = MODULATION_SPEC.updated_copy(permittivity=ST, conductivity=st_space2) + medium = td.Medium( + permittivity=3, + conductivity=1, + modulation_spec=modulation_spec, + ) + + +def test_unsupported_modulated_medium_types(): + """Unsupported types of time modulated medium""" + modulation_spec = MODULATION_SPEC.updated_copy(permittivity=ST) + + # PEC cannot be modulated + with pytest.raises(pydantic.ValidationError): + mat = td.PECMedium(modulation_spec=modulation_spec) + + # For Anisotropic medium, one should modulate the components, not the whole medium + with pytest.raises(pydantic.ValidationError): + mat = td.AnisotropicMedium( + xx=td.Medium(), yy=td.Medium(), zz=td.Medium(), modulation_spec=modulation_spec + ) + + # Modulation to fully Anisotropic medium unsupported + with pytest.raises(pydantic.ValidationError): + mat = td.FullyAnisotropicMedium(modulation_spec=modulation_spec) + + # 2D material + with pytest.raises(pydantic.ValidationError): + drude_medium = td.Drude(eps_inf=2.0, coeffs=[(1, 2), (3, 4)]) + medium2d = td.Medium2D(ss=drude_medium, tt=drude_medium, modulation_spec=modulation_spec) + + # together with nonlinear_spec + with pytest.raises(pydantic.ValidationError): + mat = td.Medium( + permittivity=2, + nonlinear_spec=td.NonlinearSusceptibility(chi3=1), + modulation_spec=modulation_spec, + ) + + +def test_supported_modulated_medium_types(): + """Supported types of time modulated medium""" + modulation_spec = MODULATION_SPEC.updated_copy(permittivity=ST) + modulation_both_spec = modulation_spec.updated_copy(conductivity=ST) + + # Dispersive + mat_p = td.PoleResidue( + eps_inf=2.0, poles=[((-1 + 2j), (3 + 4j))], modulation_spec=modulation_spec + ) + assert mat_p.time_modulated + assert isclose(mat_p.n_cfl, np.sqrt(2 - AMP_TIME)) + # too much modulation resulting in eps_inf < 0 + with pytest.raises(pydantic.ValidationError): + mat = mat_p.updated_copy(eps_inf=1.0) + # conductivity modulation + with pytest.raises(pydantic.ValidationError): + mat = mat_p.updated_copy(modulation_spec=modulation_both_spec) + mat = mat_p.updated_copy(modulation_spec=modulation_both_spec, allow_gain=True) + + # custom + permittivity = td.SpatialDataArray(np.ones((1, 1, 1)) * 2, coords=dict(x=[1], y=[1], z=[1])) + mat_c = td.CustomMedium(permittivity=permittivity, modulation_spec=modulation_spec) + assert mat_c.time_modulated + assert isclose(mat_c.n_cfl, np.sqrt(2 - AMP_TIME)) + # too much modulation resulting in eps_inf < 0 + with pytest.raises(pydantic.ValidationError): + mat = mat_c.updated_copy(permittivity=permittivity * 0.5) + # conductivity modulation + with pytest.raises(pydantic.ValidationError): + mat = mat_c.updated_copy(modulation_spec=modulation_both_spec) + mat = mat_c.updated_copy(modulation_spec=modulation_both_spec, allow_gain=True) + + # anisotropic medium component + mat = td.AnisotropicMedium(xx=td.Medium(), yy=mat_p, zz=td.Medium()) + assert mat.time_modulated + assert isclose(mat.n_cfl, np.sqrt(2 - AMP_TIME)) + + # custom anistropic medium component + mat_uc = td.CustomMedium(permittivity=permittivity) + mat = td.CustomAnisotropicMedium(xx=mat_uc, yy=mat_c, zz=mat_uc) + assert mat.time_modulated + assert isclose(mat.n_cfl, np.sqrt(2 - AMP_TIME)) diff --git a/tests/test_data/_test_datasets_no_vtk.py b/tests/test_data/_test_datasets_no_vtk.py new file mode 100644 index 000000000..f777be98f --- /dev/null +++ b/tests/test_data/_test_datasets_no_vtk.py @@ -0,0 +1,37 @@ +"""Tests tidy3d/components/data/dataset.py""" +import pytest +import builtins +from .test_datasets import test_triangular_dataset as _test_triangular_dataset +from .test_datasets import test_tetrahedral_dataset as _test_tetrahedral_dataset + + +@pytest.fixture +def hide_vtk(monkeypatch, request): + import_orig = builtins.__import__ + + def mocked_import(name, *args, **kwargs): + if name in ["vtk", "vtkmodules.vtkCommonCore"]: + raise ImportError() + return import_orig(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", mocked_import) + + +@pytest.mark.usefixtures("hide_vtk") +def test_triangular_dataset_no_vtk(tmp_path): + _test_triangular_dataset(tmp_path, "test_name") + + # double check that vtk was not imported + from tidy3d.components.types import vtk + + assert vtk["mod"] is None + + +@pytest.mark.usefixtures("hide_vtk") +def test_tetrahedral_dataset_no_vtk(tmp_path): + _test_tetrahedral_dataset(tmp_path, "test_name") + + # double check that vtk was not imported + from tidy3d.components.types import vtk + + assert vtk["mod"] is None diff --git a/tests/test_data/test_data_arrays.py b/tests/test_data/test_data_arrays.py index 00bbe7963..721ec6559 100644 --- a/tests/test_data/test_data_arrays.py +++ b/tests/test_data/test_data_arrays.py @@ -1,8 +1,10 @@ """Tests tidy3d/components/data/data_array.py""" +import pytest import numpy as np from typing import Tuple, List import tidy3d as td +from tidy3d.exceptions import DataError np.random.seed(4) @@ -277,3 +279,52 @@ def test_charge_data_array(): n = [0, 1e-12, 2e-12] p = [0, 3e-12, 4e-12] _ = td.ChargeDataArray((1 + 1j) * np.random.random((3, 3)), coords=dict(n=n, p=p)) + + +def test_point_data_array(): + _ = td.PointDataArray( + np.random.rand(2, 3), + coords=dict(index=np.arange(2), axis=np.arange(3)), + ) + + +def test_cell_data_array(): + _ = td.CellDataArray( + [[0, 1, 2], [1, 2, 3]], + coords=dict(cell_index=np.arange(2), vertex_index=np.arange(3)), + ) + + +def test_indexed_data_array(): + _ = td.IndexedDataArray( + np.random.rand(10), + coords=dict(index=np.arange(10)), + ) + + +def test_spatial_data_array(): + arr = td.SpatialDataArray( + [[[0, 1], [2, 3]], [[4, 5], [6, 7]]], + coords=dict(x=[0, 1], y=[1, 2], z=[2, 3]), + ) + + reflected = arr.reflect(axis=0, center=-0.5) + + reflected_expected = td.SpatialDataArray( + [[[4, 5], [6, 7]], [[0, 1], [2, 3]], [[0, 1], [2, 3]], [[4, 5], [6, 7]]], + coords=dict(x=[-2, -1, 0, 1], y=[1, 2], z=[2, 3]), + ) + + assert reflected == reflected_expected + + reflected = arr.reflect(axis=1, center=1) + + reflected_expected = td.SpatialDataArray( + [[[2, 3], [0, 1], [2, 3]], [[6, 7], [4, 5], [6, 7]]], + coords=dict(x=[0, 1], y=[0, 1, 2], z=[2, 3]), + ) + + assert reflected == reflected_expected + + with pytest.raises(DataError): + reflected = arr.reflect(axis=2, center=2.5) diff --git a/tests/test_data/test_datasets.py b/tests/test_data/test_datasets.py new file mode 100644 index 000000000..65ad2b4e3 --- /dev/null +++ b/tests/test_data/test_datasets.py @@ -0,0 +1,451 @@ +"""Tests tidy3d/components/data/dataset.py""" +import pytest +import numpy as np +import pydantic.v1 as pd +from matplotlib import pyplot as plt + + +np.random.seed(4) + + +@pytest.mark.parametrize("ds_name", ["test123", None]) +def test_triangular_dataset(tmp_path, ds_name): + + import tidy3d as td + from tidy3d.components.types import vtk + from tidy3d.exceptions import DataError, Tidy3dImportError + + # basic create + tri_grid_points = td.PointDataArray( + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], + dims=("index", "axis"), + ) + + tri_grid_cells = td.CellDataArray( + [[0, 1, 2], [1, 2, 3]], + dims=("cell_index", "vertex_index"), + ) + + tri_grid_values = td.IndexedDataArray( + [1.0, 2.0, 3.0, 4.0], + dims=("index"), + name=ds_name, + ) + + tri_grid = td.TriangularGridDataset( + normal_axis=1, + normal_pos=0, + points=tri_grid_points, + cells=tri_grid_cells, + values=tri_grid_values, + ) + + # test name redirect + assert tri_grid.name == ds_name + + # wrong points dimensionality + with pytest.raises(pd.ValidationError): + + tri_grid_points_bad = td.PointDataArray( + np.random.random((4, 3)), + coords=dict(index=np.arange(4), axis=np.arange(3)), + ) + + _ = td.TriangularGridDataset( + normal_axis=0, + normal_pos=10, + points=tri_grid_points_bad, + cells=tri_grid_cells, + values=tri_grid_values, + ) + + # grid with degenerate cells + tri_grid_cells_bad = td.CellDataArray( + [[0, 1, 1], [1, 2, 3]], + coords=dict(cell_index=np.arange(2), vertex_index=np.arange(3)), + ) + + _ = td.TriangularGridDataset( + normal_axis=2, + normal_pos=-3, + points=tri_grid_points, + cells=tri_grid_cells_bad, + values=tri_grid_values, + ) + + # invalid cell connections + with pytest.raises(pd.ValidationError): + + tri_grid_cells_bad = td.CellDataArray( + [[0, 1, 2, 3]], + coords=dict(cell_index=np.arange(1), vertex_index=np.arange(4)), + ) + + _ = td.TriangularGridDataset( + normal_axis=2, + normal_pos=-3, + points=tri_grid_points, + cells=tri_grid_cells_bad, + values=tri_grid_values, + ) + + with pytest.raises(pd.ValidationError): + + tri_grid_cells_bad = td.CellDataArray( + [[0, 1, 5], [1, 2, 3]], + coords=dict(cell_index=np.arange(2), vertex_index=np.arange(3)), + ) + + _ = td.TriangularGridDataset( + normal_axis=2, + normal_pos=-3, + points=tri_grid_points, + cells=tri_grid_cells_bad, + values=tri_grid_values, + ) + + # wrong number of values + with pytest.raises(pd.ValidationError): + + tri_grid_values_bad = td.IndexedDataArray( + [1.0, 2.0, 3.0], + coords=dict(index=np.arange(3)), + ) + + _ = td.TriangularGridDataset( + normal_axis=0, + normal_pos=0, + points=tri_grid_points, + cells=tri_grid_cells, + values=tri_grid_values_bad, + ) + + # some auxiliary properties + assert tri_grid.bounds == ((0.0, 0.0, 0.0), (1.0, 0.0, 1.0)) + assert np.all(tri_grid._vtk_offsets == np.array([0, 3, 6])) + + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tri_grid._vtk_cells + with pytest.raises(Tidy3dImportError): + _ = tri_grid._vtk_points + with pytest.raises(Tidy3dImportError): + _ = tri_grid._vtk_obj + else: + _ = tri_grid._vtk_cells + _ = tri_grid._vtk_points + _ = tri_grid._vtk_obj + + # plane slicing + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tri_grid.plane_slice(axis=2, pos=0.5) + else: + result = tri_grid.plane_slice(axis=2, pos=0.5) + + assert result.name == ds_name + + # can't slice parallel grid plane + with pytest.raises(DataError): + _ = tri_grid.plane_slice(axis=1, pos=0.5) + + # can't slice outside of bounds + with pytest.raises(DataError): + _ = tri_grid.plane_slice(axis=0, pos=2) + + # clipping by a box + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tri_grid.box_clip([[0.1, -0.2, 0.1], [0.2, 0.2, 0.9]]) + else: + result = tri_grid.box_clip([[0.1, -0.2, 0.1], [0.2, 0.2, 0.9]]) + assert result.name == ds_name + + # can't clip outside of grid + with pytest.raises(DataError): + _ = tri_grid.box_clip([[0.1, 0.1, 0.3], [0.2, 0.2, 0.9]]) + + # interpolation + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + invariant = tri_grid.interp( + x=0.4, y=[0, 1], z=np.linspace(0.2, 0.6, 10), fill_value=-333 + ) + else: + # default = invariant along normal direction + invariant = tri_grid.interp(x=0.4, y=[0, 1], z=np.linspace(0.2, 0.6, 10), fill_value=-333) + assert np.all(invariant.isel(y=0).data == invariant.isel(y=1).data) + assert invariant.name == ds_name + + # no invariance + out_of_plane = tri_grid.interp( + x=0.4, y=[1], z=np.linspace(0.2, 0.6, 10), fill_value=123, ignore_normal_pos=False + ) + assert np.all(out_of_plane.data == 123) + assert out_of_plane.name == ds_name + + # ouside of grid + invariant_no_intersection = tri_grid.interp( + x=[1.5, 2], y=2, z=np.linspace(0.2, 0.6, 10), fill_value=909 + ) + assert np.all(invariant_no_intersection.data == 909) + assert invariant_no_intersection.name == ds_name + + # renaming + tri_grid_renamed = tri_grid.rename("renamed") + assert tri_grid_renamed.name == "renamed" + + # plotting + _ = tri_grid.plot() + plt.close() + + _ = tri_grid.plot(grid=False) + plt.close() + + _ = tri_grid.plot(field=False) + plt.close() + + _ = tri_grid.plot(cbar=False) + plt.close() + + _ = tri_grid.plot(vmin=-20, vmax=100) + plt.close() + + _ = tri_grid.plot(cbar_kwargs=dict(label="test")) + plt.close() + + _ = tri_grid.plot(cmap="RdBu") + plt.close() + + _ = tri_grid.plot(shading="flat") + plt.close() + + with pytest.raises(DataError): + _ = tri_grid.plot(field=False, grid=False) + + # generalized selection method + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tri_grid.sel(x=0.2) + else: + _ = tri_grid.sel(x=0.2) + _ = tri_grid.sel(x=0.2, z=[0.3, 0.4, 0.5]) + result = tri_grid.sel(x=np.linspace(0, 1, 3), y=tri_grid.normal_pos, z=[0.3, 0.4, 0.5]) + assert result.name == ds_name + + # can't select out of plane + with pytest.raises(DataError): + _ = tri_grid.sel(x=np.linspace(0, 1, 3), y=1.2, z=[0.3, 0.4, 0.5]) + + # writting/reading .vtu + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + tri_grid.to_vtu(tmp_path / "tri_grid_test.vtu") + with pytest.raises(Tidy3dImportError): + tri_grid_loaded = td.TriangularGridDataset.from_vtu(tmp_path / "tri_grid_test.vtu") + else: + tri_grid.to_vtu(tmp_path / "tri_grid_test.vtu") + tri_grid_loaded = td.TriangularGridDataset.from_vtu(tmp_path / "tri_grid_test.vtu") + + assert tri_grid == tri_grid_loaded + + # test ariphmetic operations + def operation(arr): + return 5 + (arr * 2 + arr.imag / 3) ** 2 / arr.real + np.log10(arr.abs) + + result = operation(tri_grid) + result_values = operation(tri_grid.values) + + assert np.allclose(result.values, result_values) + assert result.name == ds_name + + +@pytest.mark.parametrize("ds_name", ["test123", None]) +def test_tetrahedral_dataset(tmp_path, ds_name): + + import tidy3d as td + from tidy3d.components.types import vtk + from tidy3d.exceptions import DataError, Tidy3dImportError + + # basic create + tet_grid_points = td.PointDataArray( + [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + dims=("index", "axis"), + ) + + tet_grid_cells = td.CellDataArray( + [[0, 1, 2, 4], [1, 2, 3, 4]], + dims=("cell_index", "vertex_index"), + ) + + tet_grid_values = td.IndexedDataArray( + [1.0, 2.0, 3.0, 4.0, 5.0], + dims=("index"), + name=ds_name, + ) + + tet_grid = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells, + values=tet_grid_values, + ) + + # wrong points dimensionality + with pytest.raises(pd.ValidationError): + + tet_grid_points_bad = td.PointDataArray( + np.random.random((5, 2)), + coords=dict(index=np.arange(5), axis=np.arange(2)), + ) + + _ = td.TetrahedralGridDataset( + points=tet_grid_points_bad, + cells=tet_grid_cells, + values=tet_grid_values, + ) + + # grid with degenerate cells + tet_grid_cells_bad = td.CellDataArray( + [[0, 1, 1, 4], [1, 2, 3, 4]], + coords=dict(cell_index=np.arange(2), vertex_index=np.arange(4)), + ) + + _ = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells_bad, + values=tet_grid_values, + ) + + # invalid cell connections + with pytest.raises(pd.ValidationError): + + tet_grid_cells_bad = td.CellDataArray( + [[0, 1, 2], [1, 2, 3]], + coords=dict(cell_index=np.arange(2), vertex_index=np.arange(3)), + ) + + _ = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells_bad, + values=tet_grid_values, + ) + + with pytest.raises(pd.ValidationError): + + tet_grid_cells_bad = td.CellDataArray( + [[0, 1, 2, 6], [1, 2, 3, 4]], + coords=dict(cell_index=np.arange(2), vertex_index=np.arange(4)), + ) + + _ = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells_bad, + values=tet_grid_values, + ) + + # wrong number of values + with pytest.raises(pd.ValidationError): + + tet_grid_values_bad = td.IndexedDataArray( + [1.0, 2.0, 3.0], + coords=dict(index=np.arange(3)), + ) + + _ = td.TetrahedralGridDataset( + points=tet_grid_points, + cells=tet_grid_cells_bad, + values=tet_grid_values_bad, + ) + + # some auxiliary properties + assert tet_grid.bounds == ((0.0, 0.0, 0.0), (1.0, 1.0, 1.0)) + assert np.all(tet_grid._vtk_offsets == np.array([0, 4, 8])) + assert tet_grid.name == ds_name + + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tet_grid._vtk_cells + with pytest.raises(Tidy3dImportError): + _ = tet_grid._vtk_points + with pytest.raises(Tidy3dImportError): + _ = tet_grid._vtk_obj + else: + _ = tet_grid._vtk_cells + _ = tet_grid._vtk_points + _ = tet_grid._vtk_obj + + # plane slicing + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tet_grid.plane_slice(axis=2, pos=0.5) + else: + result = tet_grid.plane_slice(axis=2, pos=0.5) + assert result.name == ds_name + + # can't slice outside of bounds + with pytest.raises(DataError): + _ = tet_grid.plane_slice(axis=1, pos=2) + + # clipping by a box + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tet_grid.box_clip([[0.1, -0.2, 0.1], [0.2, 0.2, 0.9]]) + else: + result = tet_grid.box_clip([[0.1, -0.2, 0.1], [0.2, 0.2, 0.9]]) + assert result.name == ds_name + + # can't clip outside of grid + with pytest.raises(DataError): + _ = tet_grid.box_clip([[0.1, 1.1, 0.3], [0.2, 1.2, 0.9]]) + + # interpolation + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tet_grid.interp(x=0.4, y=[0, 1], z=np.linspace(0.2, 0.6, 10), fill_value=-333) + else: + # default = invariant along normal direction + result = tet_grid.interp(x=0.4, y=[0, 1], z=np.linspace(0.2, 0.6, 10), fill_value=-333) + assert result.name == ds_name + + # outside of grid + no_intersection = tet_grid.interp( + x=[1.5, 2], y=2, z=np.linspace(0.2, 0.6, 10), fill_value=909 + ) + assert np.all(no_intersection.data == 909) + assert no_intersection.name == ds_name + + # generalized selection method + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + _ = tet_grid.sel(x=0.2) + else: + _ = tet_grid.sel(x=0.2) + _ = tet_grid.sel(x=0.2, y=0.4) + result = tet_grid.sel(x=np.linspace(0, 1, 3), y=0.55, z=[0.3, 0.4, 0.5]) + assert result.name == ds_name + + # can't do plane slicing with array of values + with pytest.raises(DataError): + _ = tet_grid.sel(x=0.2, z=[0.3, 0.4, 0.5]) + + # writting/reading .vtu + if vtk["mod"] is None: + with pytest.raises(Tidy3dImportError): + tet_grid.to_vtu(tmp_path / "tet_grid_test.vtu") + with pytest.raises(Tidy3dImportError): + tet_grid_loaded = td.TetrahedralGridDataset.from_vtu(tmp_path / "tet_grid_test.vtu") + else: + tet_grid.to_vtu(tmp_path / "tet_grid_test.vtu") + tet_grid_loaded = td.TetrahedralGridDataset.from_vtu(tmp_path / "tet_grid_test.vtu") + + assert tet_grid == tet_grid_loaded + + # test ariphmetic operations + def operation(arr): + return 5 + (arr * 2 + arr.imag / 3) ** 2 / arr.real + np.log10(arr.abs) + + result = operation(tet_grid) + result_values = operation(tet_grid.values) + + assert np.allclose(result.values, result_values) + assert result.name == ds_name diff --git a/tests/test_data/test_monitor_data.py b/tests/test_data/test_monitor_data.py index 45e9ff332..09e30949c 100644 --- a/tests/test_data/test_monitor_data.py +++ b/tests/test_data/test_monitor_data.py @@ -529,3 +529,20 @@ def test_outer_dot(): _ = field_data.outer_dot(mode_data) _ = mode_data.outer_dot(field_data) _ = field_data.outer_dot(field_data) + + +@pytest.mark.parametrize("phase_shift", np.linspace(0, 2 * np.pi, 10)) +def test_field_data_phase(phase_shift): + def get_combined_phase(data): + field_sum = 0.0 + for fld_cmp in data.field_components.values(): + field_sum += np.sum(fld_cmp.values) + return np.angle(field_sum) + + fld_data1 = make_field_data() + fld_data2 = fld_data1.apply_phase(phase_shift) + + phase1 = get_combined_phase(fld_data1) + phase2 = get_combined_phase(fld_data2) + + assert np.allclose(phase2, np.angle(np.exp(1j * (phase1 + phase_shift)))) diff --git a/tests/test_data/test_sim_data.py b/tests/test_data/test_sim_data.py index 2fe4c0af0..52a3cfcdb 100644 --- a/tests/test_data/test_sim_data.py +++ b/tests/test_data/test_sim_data.py @@ -119,7 +119,8 @@ def test_centers(): _ = sim_data.at_centers(mon.name) -def test_plot(): +@pytest.mark.parametrize("phase", [0, 1.0]) +def test_plot(phase): sim_data = make_sim_data() # plot regular field data @@ -127,11 +128,13 @@ def test_plot(): field_data = sim_data["field"].field_components[field_cmp] for axis_name in "xyz": xyz_kwargs = {axis_name: field_data.coords[axis_name][0]} - _ = sim_data.plot_field("field", field_cmp, val="imag", f=1e14, **xyz_kwargs) + _ = sim_data.plot_field( + "field", field_cmp, val="imag", f=1e14, phase=phase, **xyz_kwargs + ) plt.close() for axis_name in "xyz": xyz_kwargs = {axis_name: 0} - _ = sim_data.plot_field("field", "int", f=1e14, **xyz_kwargs) + _ = sim_data.plot_field("field", "int", f=1e14, phase=phase, **xyz_kwargs) plt.close() # plot field time data @@ -139,18 +142,22 @@ def test_plot(): field_data = sim_data["field_time"].field_components[field_cmp] for axis_name in "xyz": xyz_kwargs = {axis_name: field_data.coords[axis_name][0]} - _ = sim_data.plot_field("field_time", field_cmp, val="real", t=0.0, **xyz_kwargs) + _ = sim_data.plot_field( + "field_time", field_cmp, val="real", phase=phase, t=0.0, **xyz_kwargs + ) plt.close() for axis_name in "xyz": xyz_kwargs = {axis_name: 0} - _ = sim_data.plot_field("field_time", "int", t=0.0, **xyz_kwargs) + _ = sim_data.plot_field("field_time", "int", t=0.0, phase=phase, **xyz_kwargs) plt.close() # plot mode field data for field_cmp in ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz"): - _ = sim_data.plot_field("mode_solver", field_cmp, val="real", f=1e14, mode_index=1) + _ = sim_data.plot_field( + "mode_solver", field_cmp, val="real", f=1e14, mode_index=1, phase=phase + ) plt.close() - _ = sim_data.plot_field("mode_solver", "int", f=1e14, mode_index=1) + _ = sim_data.plot_field("mode_solver", "int", f=1e14, mode_index=1, phase=phase) plt.close() diff --git a/tests/test_package/test_log.py b/tests/test_package/test_log.py index f18a78f04..b0a2a7d47 100644 --- a/tests/test_package/test_log.py +++ b/tests/test_package/test_log.py @@ -74,7 +74,7 @@ def test_logging_warning_capture(): mode_mnt = td.ModeMonitor( center=(0, 0, 0), size=(domain_size, 0, domain_size), - freqs=list(freqs) + [0.1], + freqs=list(freqs) + [0.1e6], mode_spec=td.ModeSpec(num_modes=3), name="mode", ) diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index 57609bbe9..addad8087 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -20,8 +20,12 @@ from tidy3d.plugins.adjoint.components.geometry import JaxGeometryGroup from tidy3d.plugins.adjoint.components.medium import JaxMedium, JaxAnisotropicMedium from tidy3d.plugins.adjoint.components.medium import JaxCustomMedium, MAX_NUM_CELLS_CUSTOM_MEDIUM -from tidy3d.plugins.adjoint.components.structure import JaxStructure -from tidy3d.plugins.adjoint.components.simulation import JaxSimulation, JaxInfo +from tidy3d.plugins.adjoint.components.structure import ( + JaxStructure, + JaxStructureStaticMedium, + JaxStructureStaticGeometry, +) +from tidy3d.plugins.adjoint.components.simulation import JaxSimulation, JaxInfo, RUN_TIME_FACTOR from tidy3d.plugins.adjoint.components.simulation import MAX_NUM_INPUT_STRUCTURES from tidy3d.plugins.adjoint.components.data.sim_data import JaxSimulationData from tidy3d.plugins.adjoint.components.data.monitor_data import JaxModeData, JaxDiffractionData @@ -32,8 +36,8 @@ from tidy3d.plugins.adjoint.components.data.data_array import VALUE_FILTER_THRESHOLD from tidy3d.plugins.adjoint.utils.penalty import RadiusPenalty from tidy3d.plugins.adjoint.utils.filter import ConicFilter, BinaryProjector, CircularFilter -from tidy3d.web.container import BatchData - +from tidy3d.web.api.container import BatchData +import tidy3d.material_library as material_library from ..utils import run_emulated, assert_log_level, log_capture, run_async_emulated from ..test_components.test_custom import CUSTOM_MEDIUM @@ -54,6 +58,12 @@ # name of the output monitor used in tests MNT_NAME = "mode" +src = td.PointDipole( + center=(0, 0, 0), + source_time=td.GaussianPulse(freq0=FREQ0, fwidth=FREQ0 / 10), + polarization="Ex", +) + # Emulated forward and backward run functions def run_emulated_fwd( simulation: td.Simulation, @@ -247,6 +257,14 @@ def make_sim( jax_geo_group = JaxGeometryGroup(geometries=[jax_polyslab1, jax_polyslab1]) jax_struct_group = JaxStructure(geometry=jax_geo_group, medium=jax_med1) + + jax_struct_static_med = JaxStructureStaticMedium( + geometry=jax_box1, medium=td.Medium() # material_library["Ag"]["Rakic1998BB"] + ) + jax_struct_static_geo = JaxStructureStaticGeometry( + geometry=td.Box(size=(1, 1, 1)), medium=jax_med1 + ) + # TODO: Add new geometries as they are created. # NOTE: Any new output monitors should be added below as they are made @@ -255,7 +273,7 @@ def make_sim( output_mnt1 = td.ModeMonitor( size=(10, 10, 0), mode_spec=td.ModeSpec(num_modes=3), - freqs=[FREQ0], + freqs=[FREQ0, FREQ0 * 1.1], name=MNT_NAME + "1", ) @@ -276,13 +294,13 @@ def make_sim( output_mnt4 = td.FieldMonitor( size=(0, 0, 0), - freqs=[FREQ0], + freqs=np.array([FREQ0, FREQ0 * 1.1]), name=MNT_NAME + "4", ) extraneous_field_monitor = td.FieldMonitor( size=(10, 10, 0), - freqs=[1e14, 2e14], + freqs=np.array([1e14, 2e14]), name="field", ) @@ -299,8 +317,11 @@ def make_sim( jax_struct3, jax_struct_group, jax_struct_custom_anis, + jax_struct_static_med, + jax_struct_static_geo, ), output_monitors=(output_mnt1, output_mnt2, output_mnt3, output_mnt4), + sources=[src], boundary_spec=td.BoundarySpec.pml(x=False, y=False, z=False), symmetry=(0, 1, -1), ) @@ -550,23 +571,8 @@ def _test_adjoint_setup_adj(use_emulated_run): assert len(sim_vjp.input_structures) == len(sim_orig.input_structures) -# @pytest.mark.parametrize("add_grad_monitors", (True, False)) -# def test_convert_tidy3d_to_jax(add_grad_monitors): -# """test conversion of JaxSimulation to Simulation and SimulationData to JaxSimulationData.""" -# jax_sim = make_sim(permittivity=EPS, size=SIZE, vertices=VERTICES, base_eps_val=BASE_EPS_VAL) -# if add_grad_monitors: -# jax_sim = jax_sim.add_grad_monitors() -# sim, jax_info = jax_sim.to_simulation() -# assert type(sim) == td.Simulation -# assert sim.type == "Simulation" -# sim_data = run_emulated(sim) -# jax_sim_data = JaxSimulationData.from_sim_data(sim_data, jax_info) -# jax_sim2 = jax_sim_data.simulation -# assert jax_sim_data.simulation == jax_sim - - def test_multiple_freqs(): - """Test that sim validation fails when output monitors have multiple frequencies.""" + """Test that sim validation doesnt fail when output monitors have multiple frequencies.""" output_mnt = td.ModeMonitor( size=(10, 10, 0), @@ -575,20 +581,19 @@ def test_multiple_freqs(): name=MNT_NAME, ) - with pytest.raises(pydantic.ValidationError): - _ = JaxSimulation( - size=(10, 10, 10), - run_time=1e-12, - grid_spec=td.GridSpec(wavelength=1.0), - monitors=(), - structures=(), - output_monitors=(output_mnt,), - input_structures=(), - ) + _ = JaxSimulation( + size=(10, 10, 10), + run_time=1e-12, + grid_spec=td.GridSpec(wavelength=1.0), + monitors=(), + structures=(), + output_monitors=(output_mnt,), + input_structures=(), + ) def test_different_freqs(): - """Test that sim validation fails when output monitors have different frequencies.""" + """Test that sim validation doesnt fail when output monitors have different frequencies.""" output_mnt1 = td.ModeMonitor( size=(10, 10, 0), @@ -602,16 +607,15 @@ def test_different_freqs(): freqs=[2e14], name=MNT_NAME + "2", ) - with pytest.raises(pydantic.ValidationError): - _ = JaxSimulation( - size=(10, 10, 10), - run_time=1e-12, - grid_spec=td.GridSpec(wavelength=1.0), - monitors=(), - structures=(), - output_monitors=(output_mnt1, output_mnt2), - input_structures=(), - ) + _ = JaxSimulation( + size=(10, 10, 10), + run_time=1e-12, + grid_spec=td.GridSpec(wavelength=1.0), + monitors=(), + structures=(), + output_monitors=(output_mnt1, output_mnt2), + input_structures=(), + ) def test_get_freq_adjoint(): @@ -628,9 +632,11 @@ def test_get_freq_adjoint(): ) with pytest.raises(AdjointError): - _ = sim.freq_adjoint + _ = sim.freqs_adjoint freq0 = 2e14 + freq1 = 3e14 + freq2 = 1e14 output_mnt1 = td.ModeMonitor( size=(10, 10, 0), mode_spec=td.ModeSpec(num_modes=3), @@ -640,7 +646,7 @@ def test_get_freq_adjoint(): output_mnt2 = td.ModeMonitor( size=(10, 10, 0), mode_spec=td.ModeSpec(num_modes=3), - freqs=[freq0], + freqs=[freq1, freq2, freq0], name=MNT_NAME + "2", ) sim = JaxSimulation( @@ -652,7 +658,11 @@ def test_get_freq_adjoint(): output_monitors=(output_mnt1, output_mnt2), input_structures=(), ) - assert sim.freq_adjoint == freq0 + + freqs = [freq0, freq1, freq2] + freqs.sort() + + assert sim.freqs_adjoint == freqs def test_get_fwidth_adjoint(): @@ -691,7 +701,7 @@ def make_sim(sources=(), fwidth_adjoint=None): src_times = [td.GaussianPulse(freq0=freq0, fwidth=fwidth) for fwidth in fwidths] srcs = [td.PointDipole(source_time=src_time, polarization="Ex") for src_time in src_times] sim = make_sim(sources=srcs, fwidth_adjoint=None) - assert np.isclose(sim._fwidth_adjoint, np.mean(fwidths)) + assert np.isclose(sim._fwidth_adjoint, np.max(fwidths)) # a few sources, with custom fwidth specified fwidth_custom = 3e13 @@ -853,6 +863,11 @@ def test_structure_overlaps(): grid_spec=td.GridSpec(wavelength=1.0), run_time=1e-12, sources=(src,), + boundary_spec=td.BoundarySpec( + x=td.Boundary.pml(), + y=td.Boundary.periodic(), + z=td.Boundary.pml(), + ), ) @@ -1472,6 +1487,20 @@ def test_adjoint_utils(strict_binarize): _ = radius_penalty.evaluate(polyslab.vertices) +@pytest.mark.parametrize( + "input_size_y, log_level_expected", [(13, None), (12, "WARNING"), (11, "WARNING"), (14, None)] +) +def test_adjoint_filter_sizes(log_capture, input_size_y, log_level_expected): + """Warn if filter size along a dim is smaller than radius.""" + + signal_in = np.ones((266, input_size_y)) + + _filter = ConicFilter(radius=0.08, design_region_dl=0.015) + _filter.evaluate(signal_in) + + assert_log_level(log_capture, log_level_expected) + + def test_sim_data_plot_field(use_emulated_run): """Test splitting of regular simulation data into user and server data.""" @@ -1548,3 +1577,126 @@ def f(x): return jnp.sum(jnp.abs(jnp.array(sd["test"].amps.values))) jax.grad(f)(0.5) + + +fwidth_run_time_expected = [ + (FREQ0 / 10, 1e-11, 1e-11), # run time supplied explicitly, use that + (FREQ0 / 10, None, RUN_TIME_FACTOR / (FREQ0 / 10)), # no run_time, use fwidth supplied + (FREQ0 / 20, None, RUN_TIME_FACTOR / (FREQ0 / 20)), # no run_time, use fwidth supplied +] + + +@pytest.mark.parametrize("fwidth, run_time, run_time_expected", fwidth_run_time_expected) +def test_adjoint_run_time(use_emulated_run, tmp_path, fwidth, run_time, run_time_expected): + + sim = make_sim(permittivity=EPS, size=SIZE, vertices=VERTICES, base_eps_val=BASE_EPS_VAL) + + sim = sim.updated_copy(run_time_adjoint=run_time, fwidth_adjoint=fwidth) + + sim_data = run(sim, task_name="test", path=str(tmp_path / RUN_FILE)) + + run_time_adj = sim._run_time_adjoint + fwidth_adj = sim._fwidth_adjoint + + sim_adj = sim_data.make_adjoint_simulation(fwidth=fwidth_adj, run_time=run_time_adj) + + assert sim_adj.run_time == run_time_expected + + +@pytest.mark.parametrize("has_adj_src, log_level_expected", [(True, None), (False, "WARNING")]) +def test_no_adjoint_sources( + monkeypatch, use_emulated_run, tmp_path, log_capture, has_adj_src, log_level_expected +): + """Make sure warning (not error) if no adjoint sources.""" + + def make_sim(eps): + """Make a sim with given sources and fwidth_adjoint specified.""" + struct = JaxStructure( + geometry=JaxBox(center=(0, 0, 0), size=(1, 1, 1)), + medium=JaxMedium(permittivity=eps), + ) + + freq0 = 2e14 + mnt = td.ModeMonitor( + size=(10, 10, 0), + mode_spec=td.ModeSpec(num_modes=3), + freqs=[freq0], + name="mnt", + ) + + return JaxSimulation( + size=(10, 10, 10), + run_time=1e-12, + grid_spec=td.GridSpec(wavelength=1.0), + monitors=(), + structures=(), + output_monitors=(mnt,), + input_structures=(), + sources=[src], + ) + + if not has_adj_src: + monkeypatch.setattr(JaxModeData, "to_adjoint_sources", lambda *args, **kwargs: []) + + def J(x): + sim = make_sim(eps=x) + data = run(sim, task_name="test", path=str(tmp_path / RUN_FILE)) + data.make_adjoint_simulation(fwidth=src.source_time.fwidth, run_time=sim.run_time) + power = jnp.sum(jnp.abs(jnp.array(data["mnt"].amps.values)) ** 2) + return power + + grad_J = grad(J) + grad_J(2.0) + assert_log_level(log_capture, log_level_expected) + + +def test_nonlinear_warn(log_capture): + """Test that simulations warn if nonlinearity is used.""" + + struct = JaxStructure( + geometry=JaxBox(center=(0, 0, 0), size=(1, 1, 1)), + medium=JaxMedium(permittivity=2.0), + ) + + struct_static = td.Structure( + geometry=td.Box(center=(0, 3, 0), size=(1, 1, 1)), + medium=td.Medium(permittivity=2.0), + ) + + sim_base = JaxSimulation( + size=(10, 10, 0), + run_time=1e-12, + grid_spec=td.GridSpec(wavelength=1.0), + monitors=(), + structures=(struct_static,), + output_monitors=(), + input_structures=(struct,), + sources=[src], + boundary_spec=td.BoundarySpec.pml(x=True, y=True, z=False), + ) + + # make the nonlinear objects to add to the JaxSimulation one by one + nl_model = td.KerrNonlinearity(n2=1) + nl_medium = td.Medium(nonlinear_spec=td.NonlinearSpec(models=[nl_model])) + struct_static_nl = struct_static.updated_copy(medium=nl_medium) + input_struct_nl = JaxStructureStaticMedium(geometry=struct.geometry, medium=nl_medium) + + def test_log_level(desired_level): + """Convenience function to test the log level and clear it.""" + assert_log_level(log_capture, desired_level) + log_capture.clear() + + # no nonlinearity (no warning) + test_log_level(None) + + # nonlinear simulation.medium (error) + sim = sim_base.updated_copy(medium=nl_medium) + test_log_level("WARNING") + + # nonlinear structure (warn) + sim = sim_base.updated_copy(structures=[struct_static_nl]) + test_log_level("WARNING") + + # nonlinear input_structure (warn) + sim = sim_base.updated_copy(input_structures=[input_struct_nl]) + test_log_level("WARNING") diff --git a/tests/test_plugins/test_component_modeler.py b/tests/test_plugins/test_component_modeler.py index a24154bcf..c65a74514 100644 --- a/tests/test_plugins/test_component_modeler.py +++ b/tests/test_plugins/test_component_modeler.py @@ -5,7 +5,7 @@ import gdstk import tidy3d as td -from tidy3d.web.container import Batch +from tidy3d.web.api.container import Batch from tidy3d.plugins.smatrix.smatrix import Port, ComponentModeler from tidy3d.exceptions import SetupError, Tidy3dKeyError from ..utils import run_emulated @@ -254,6 +254,12 @@ def test_plot_sim(tmp_path): plt.close() +def test_plot_sim_eps(tmp_path): + modeler = make_component_modeler(path_dir=str(tmp_path)) + modeler.plot_sim_eps(z=0) + plt.close() + + def test_make_component_modeler(tmp_path): _ = make_component_modeler(path_dir=str(tmp_path)) @@ -369,3 +375,8 @@ def test_mapping_exclusion(monkeypatch, tmp_path): s_matrix = run_component_modeler(monkeypatch, modeler) _test_mappings(element_mappings, s_matrix) + + +def test_batch_filename(tmp_path): + modeler = make_component_modeler(path_dir=str(tmp_path)) + path = modeler._batch_path diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 5942c9b75..6a77d3a72 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -6,16 +6,15 @@ import tidy3d as td -from tidy3d.version import __version__ import tidy3d.plugins.mode.web as msweb from tidy3d.plugins.mode import ModeSolver from tidy3d.plugins.mode.mode_solver import MODE_MONITOR_NAME from tidy3d.plugins.mode.derivatives import create_sfactor_b, create_sfactor_f from tidy3d.plugins.mode.solver import compute_modes +from tidy3d.exceptions import SetupError from ..utils import assert_log_level, log_capture from tidy3d import ScalarFieldDataArray -from tidy3d.web.environment import Env -from tidy3d.version import __version__ +from tidy3d.web.core.environment import Env WG_MEDIUM = td.Medium(permittivity=4.0, conductivity=1e-4) @@ -64,9 +63,12 @@ def mock_download(task_id, remote_path, to_file, *args, **kwargs): ) ms.data_raw.to_file(to_file) - monkeypatch.setattr(td.web.http_management, "api_key", lambda: "api_key") - monkeypatch.setattr("tidy3d.plugins.mode.web.upload_file", void) - monkeypatch.setattr("tidy3d.plugins.mode.web.download_file", mock_download) + from tidy3d.web.core import http_util as httputil + + monkeypatch.setattr(httputil, "api_key", lambda: "api_key") + monkeypatch.setattr(httputil, "get_version", lambda: td.version.__version__) + monkeypatch.setattr("tidy3d.web.api.mode.upload_file", void) + monkeypatch.setattr("tidy3d.web.api.mode.download_file", mock_download) responses.add( responses.GET, @@ -87,7 +89,7 @@ def mock_download(task_id, remote_path, to_file, *args, **kwargs): "modeSolverName": MODESOLVER_NAME, "fileType": "Gz", "source": "Python", - "protocolVersion": __version__, + "protocolVersion": td.version.__version__, } ) ], @@ -161,7 +163,7 @@ def compare_colocation(ms): for key, field in data_col.field_components.items(): # Check the colocated data is the same - assert np.allclose(data_at_boundaries[key], field) + assert np.allclose(data_at_boundaries[key], field, atol=1e-7) # Also check coordinates for dim, coords1 in field.coords.items(): @@ -202,6 +204,15 @@ def verify_pol_fraction(ms): ) +def verify_dtype(ms): + """Verify that the returned fields have the correct dtype w.r.t. the specified precision.""" + + dtype = np.complex64 if ms.mode_spec.precision == "single" else np.complex128 + for field in ms.data.field_components.values(): + print(dtype, field.dtype, type(field.dtype)) + assert dtype == field.dtype + + def test_mode_solver_validation(): """Test invalidate mode solver setups.""" @@ -233,6 +244,17 @@ def test_mode_solver_validation(): direction="+", ) + # mode data too large + simulation = td.Simulation( + size=SIM_SIZE, + grid_spec=td.GridSpec.uniform(dl=0.001), + run_time=1e-12, + ) + ms = ms.updated_copy(simulation=simulation, freqs=np.linspace(1e12, 2e12, 50)) + + with pytest.raises(SetupError): + ms.validate_pre_upload() + @pytest.mark.parametrize("group_index_step, log_level", ((1e-7, "WARNING"), (1e-5, "INFO"))) def test_mode_solver_group_index_warning(group_index_step, log_level, log_capture): @@ -294,6 +316,7 @@ def test_mode_solver_simple(mock_remote_api, local): if local: compare_colocation(ms) verify_pol_fraction(ms) + verify_dtype(ms) dataframe = ms.data.to_dataframe() else: @@ -457,6 +480,7 @@ def test_mode_solver_angle_bend(): ) compare_colocation(ms) verify_pol_fraction(ms) + verify_dtype(ms) dataframe = ms.data.to_dataframe() # Plot field @@ -465,9 +489,9 @@ def test_mode_solver_angle_bend(): plt.close() # Create source and monitor - st = td.GaussianPulse(freq0=1.0, fwidth=1.0) + st = td.GaussianPulse(freq0=1.0e12, fwidth=1.0e12) _ = ms.to_source(source_time=st, direction="-") - _ = ms.to_monitor(freqs=[1.0, 2.0], name="mode_mnt") + _ = ms.to_monitor(freqs=np.array([1.0, 2.0]) * 1e12, name="mode_mnt") def test_mode_solver_2D(): @@ -492,6 +516,7 @@ def test_mode_solver_2D(): ) compare_colocation(ms) verify_pol_fraction(ms) + verify_dtype(ms) dataframe = ms.data.to_dataframe() mode_spec = td.ModeSpec( @@ -530,7 +555,7 @@ def test_mode_solver_2D(): @pytest.mark.parametrize("local", [True, False]) @responses.activate -def test_group_index(mock_remote_api, local): +def test_group_index(mock_remote_api, log_capture, local): """Test group index calculation""" simulation = td.Simulation( @@ -569,6 +594,11 @@ def test_group_index(mock_remote_api, local): modes = ms.solve() if local else msweb.run(ms) if local: assert modes.n_group is None + assert len(log_capture) == 1 + assert log_capture[0][0] == 30 + assert "ModeSpec" in log_capture[0][1] + _ = modes.n_group + assert len(log_capture) == 1 # Group index calculated ms = ModeSolver( @@ -609,3 +639,94 @@ def test_pml_params(): sf_f = create_sfactor_f(omega, dls, N, n_pml, dmin_pml=True) assert np.allclose(sf_f[:n_pml] / sf_f[n_pml - 1], target_profile[::-1]) assert np.allclose(sf_f[N - n_pml :] / sf_f[N - n_pml], target_profile) + + +def test_mode_solver_nan_pol_fraction(): + """Test mode solver when eigensolver returns 0 for some modes.""" + wg = td.Structure(geometry=td.Box(size=(0.5, 100, 0.22)), medium=td.Medium(permittivity=12)) + + simulation = td.Simulation( + medium=td.Medium(permittivity=2), + size=SIM_SIZE, + grid_spec=td.GridSpec.auto(wavelength=1.55, min_steps_per_wvl=15), + structures=[wg], + run_time=1e-12, + symmetry=(0, 0, 1), + boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), + sources=[SRC], + ) + + mode_spec = td.ModeSpec( + num_modes=10, + target_neff=3.48, + filter_pol="tm", + precision="single", + track_freq="central", + ) + + freqs = [td.C_0 / 1.55] + + ms = ModeSolver( + simulation=simulation, + plane=td.Box(center=(0, 0, 0), size=(2, 0, 1.1)), + mode_spec=mode_spec, + freqs=freqs, + direction="-", + ) + + md = ms.solve() + + assert list(np.where(np.isnan(md.pol_fraction.te))[1]) == [8, 9] + + +def test_mode_solver_method_defaults(): + """Test that changes to mode solver default values in methods work.""" + + simulation = td.Simulation( + medium=td.Medium(permittivity=2), + size=SIM_SIZE, + grid_spec=td.GridSpec.auto(wavelength=1.55, min_steps_per_wvl=15), + run_time=1e-12, + symmetry=(0, 0, 1), + boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), + sources=[SRC], + ) + + mode_spec = td.ModeSpec( + num_modes=10, + target_neff=3.48, + filter_pol="tm", + precision="single", + track_freq="central", + ) + + freqs = [td.C_0 / 1.55] + + ms = ModeSolver( + simulation=simulation, + plane=td.Box(center=(0, 0, 0), size=(2, 0, 1.1)), + mode_spec=mode_spec, + freqs=freqs, + direction="-", + ) + + # test defaults + st = td.GaussianPulse(freq0=1.0e12, fwidth=1.0e12) + + src = ms.to_source(source_time=st) + assert src.direction == ms.direction + + src = ms.to_source(source_time=st, direction="+") + assert src.direction != ms.direction + + mnt = ms.to_monitor(name="mode_mnt") + assert np.allclose(mnt.freqs, ms.freqs) + + mnt = ms.to_monitor(name="mode_mnt", freqs=[2e14]) + assert not np.allclose(mnt.freqs, ms.freqs) + + sim = ms.sim_with_source(source_time=st) + assert sim.sources[-1].direction == ms.direction + + sim = ms.sim_with_monitor(name="test") + assert np.allclose(sim.monitors[-1].freqs, ms.freqs) diff --git a/tests/test_web/test_material_fitter.py b/tests/test_web/test_material_fitter.py index 88bdf3b3e..5cc03ebe2 100644 --- a/tests/test_web/test_material_fitter.py +++ b/tests/test_web/test_material_fitter.py @@ -1,10 +1,12 @@ import pytest import responses + from tidy3d.plugins.dispersion import DispersionFitter -from tidy3d.web.environment import Env -from tidy3d.web.material_fitter import FitterOptions, MaterialFitterTask +from tidy3d.web.core.environment import Env +from tidy3d.web.api.material_fitter import FitterOptions, MaterialFitterTask +import tidy3d as td Env.dev.active() @@ -12,16 +14,17 @@ @pytest.fixture def set_api_key(monkeypatch): """Set the api key.""" - import tidy3d.web.http_management as http_module + import tidy3d.web.core.http_util as httputil - monkeypatch.setattr(http_module, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "get_version", lambda: td.version.__version__) @responses.activate -def test_material_fitter(monkeypatch, set_api_key): +def test_material_fitter(tmp_path, monkeypatch, set_api_key): fitter = DispersionFitter.from_file("tests/data/nk_data.csv", skiprows=1, delimiter=",") - monkeypatch.setattr("tidy3d.web.material_fitter.uuid4", lambda: "fitter_id") + monkeypatch.setattr("tidy3d.web.api.material_fitter.uuid4", lambda: "fitter_id") responses.add( responses.GET, @@ -36,7 +39,7 @@ def test_material_fitter(monkeypatch, set_api_key): status=200, ) monkeypatch.setattr( - "tidy3d.web.material_fitter.MaterialFitterTask.submit", + "tidy3d.web.api.material_fitter.MaterialFitterTask.submit", lambda fitter, options: MaterialFitterTask( id="1234", status="ok", dispersion_fitter=fitter, resourcePath="", fileName="" ), @@ -63,7 +66,7 @@ def test_material_fitter(monkeypatch, set_api_key): status=200, ) monkeypatch.setattr( - "tidy3d.web.material_fitter.MaterialFitterTask.sync_status", lambda self: None + "tidy3d.web.api.material_fitter.MaterialFitterTask.sync_status", lambda self: None ) task.sync_status() task.status == "running" diff --git a/tests/test_web/test_task.py b/tests/test_web/test_task.py index 626a2f11e..cd2811c36 100644 --- a/tests/test_web/test_task.py +++ b/tests/test_web/test_task.py @@ -1,4 +1,4 @@ -from tidy3d.web.task import RunInfo +from tidy3d.web.core.task_info import RunInfo def test_run_info_display(): diff --git a/tests/test_web/test_tidy3d_folder.py b/tests/test_web/test_tidy3d_folder.py index be42e07b5..4bf885ae7 100644 --- a/tests/test_web/test_tidy3d_folder.py +++ b/tests/test_web/test_tidy3d_folder.py @@ -1,9 +1,9 @@ import pytest import responses from responses import matchers - -from tidy3d.web.simulation_task import Folder -from tidy3d.web.environment import Env +import tidy3d as td +from tidy3d.web.core.task_core import Folder +from tidy3d.web.core.environment import Env Env.dev.active() @@ -11,9 +11,10 @@ @pytest.fixture def set_api_key(monkeypatch): """Set the api key.""" - import tidy3d.web.http_management as http_module + import tidy3d.web.core.http_util as httputil - monkeypatch.setattr(http_module, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "get_version", lambda: td.version.__version__) @responses.activate @@ -42,17 +43,18 @@ def test_get_folder(set_api_key): @responses.activate def test_create_and_remove_folder(set_api_key): + folder_name = "test folder2" responses.add( responses.GET, - f"{Env.current.web_api_endpoint}/tidy3d/project", - match=[matchers.query_param_matcher({"projectName": "test folder2"})], - status=404, + f"{Env.current.web_api_endpoint}/tidy3d/project?projectName={folder_name}", + json={"data": {"projectId": "1234", "projectName": "default"}}, + status=200, ) responses.add( responses.POST, f"{Env.current.web_api_endpoint}/tidy3d/projects", - match=[matchers.json_params_matcher({"projectName": "test folder2"})], - json={"data": {"projectId": "1234", "projectName": "test folder2"}}, + match=[matchers.json_params_matcher({"projectName": folder_name})], + json={"data": {"projectId": "1234", "projectName": folder_name}}, status=200, ) responses.add( @@ -60,7 +62,8 @@ def test_create_and_remove_folder(set_api_key): f"{Env.current.web_api_endpoint}/tidy3d/projects/1234", status=200, ) - resp = Folder.create("test folder2") + + resp = Folder.create(folder_name) assert resp is not None resp.delete() diff --git a/tests/test_web/test_tidy3d_material_library.py b/tests/test_web/test_tidy3d_material_library.py index ea6ad6ed2..14bcdf5cd 100644 --- a/tests/test_web/test_tidy3d_material_library.py +++ b/tests/test_web/test_tidy3d_material_library.py @@ -1,8 +1,9 @@ import pytest import responses -from tidy3d.web.environment import Env -from tidy3d.web.material_libray import MaterialLibray +from tidy3d.web.core.environment import Env +from tidy3d.web.api.material_libray import MaterialLibray +import tidy3d as td Env.dev.active() @@ -10,9 +11,10 @@ @pytest.fixture def set_api_key(monkeypatch): """Set the api key.""" - import tidy3d.web.http_management as http_module + import tidy3d.web.core.http_util as httputil - monkeypatch.setattr(http_module, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "get_version", lambda: td.version.__version__) @responses.activate diff --git a/tests/test_web/test_tidy3d_task.py b/tests/test_web/test_tidy3d_task.py index 7e868cbf1..c7558c242 100644 --- a/tests/test_web/test_tidy3d_task.py +++ b/tests/test_web/test_tidy3d_task.py @@ -5,9 +5,10 @@ from responses import matchers import tidy3d as td -from tidy3d.web.environment import Env, EnvironmentConfig -from tidy3d.web.simulation_task import Folder, SimulationTask -from tidy3d.version import __version__ +from tidy3d.web.core import http_util +from tidy3d.web.core.environment import Env, EnvironmentConfig +from tidy3d.web.core.task_core import Folder, SimulationTask +from tidy3d.web.core.types import TaskType test_env = EnvironmentConfig( name="test", @@ -34,9 +35,10 @@ def make_sim(): @pytest.fixture def set_api_key(monkeypatch): """Set the api key.""" - import tidy3d.web.http_management as http_module + import tidy3d.web.core.http_util as httputil - monkeypatch.setattr(http_module, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "api_key", lambda: "apikey") + monkeypatch.setattr(httputil, "get_version", lambda: td.version.__version__) @responses.activate @@ -78,19 +80,6 @@ def test_query_task(set_api_key): task = SimulationTask.get("3eb06d16-208b-487b-864b-e9b1d3e010a7") assert task - responses.add( - responses.GET, - f"{Env.current.web_api_endpoint}/tidy3d/tasks/xxx/detail", - json={ - "data": { - "taskId": "3eb06d16-208b-487b-864b-e9b1d3e010a7", - "createdAt": "2022-01-01T00:00:00.000Z", - } - }, - status=404, - ) - assert SimulationTask.get("xxx") is None - @responses.activate def test_get_simulation_json(monkeypatch, set_api_key, tmp_path): @@ -100,7 +89,7 @@ def mock_download(*args, **kwargs): to_file = kwargs["to_file"] sim.to_file(to_file) - monkeypatch.setattr("tidy3d.web.simulation_task.download_file", mock_download) + monkeypatch.setattr("tidy3d.web.core.task_core.download_file", mock_download) responses.add( responses.GET, @@ -136,7 +125,7 @@ def test_upload(monkeypatch, set_api_key): def mock_download(*args, **kwargs): pass - monkeypatch.setattr("tidy3d.web.simulation_task.upload_file", mock_download) + monkeypatch.setattr("tidy3d.web.core.task_core.upload_file", mock_download) task = SimulationTask.get("3eb06d16-208b-487b-864b-e9b1d3e010a7") with tempfile.NamedTemporaryFile() as temp: task.upload_file(temp.name, "temp.json") @@ -144,6 +133,7 @@ def mock_download(*args, **kwargs): @responses.activate def test_create(set_api_key): + task_id = "1234" responses.add( responses.GET, f"{Env.current.web_api_endpoint}/tidy3d/project", @@ -153,10 +143,11 @@ def test_create(set_api_key): ) responses.add( responses.POST, - f"{Env.current.web_api_endpoint}/tidy3d/projects/1234/tasks", + f"{Env.current.web_api_endpoint}/tidy3d/projects/{task_id}/tasks", match=[ matchers.json_params_matcher( { + "taskType": TaskType.FDTD, "taskName": "test task", "callbackUrl": None, "fileType": "Gz", @@ -167,21 +158,21 @@ def test_create(set_api_key): ], json={ "data": { - "taskId": "1234", + "taskId": task_id, "taskName": "test task", "createdAt": "2022-01-01T00:00:00.000Z", } }, status=200, ) - task = SimulationTask.create(None, "test task", "test folder2") - assert task.task_id == "1234" + task = SimulationTask.create(TaskType.FDTD, "test task", "test folder2") + assert task.task_id == task_id @responses.activate def test_submit(set_api_key): project_id = "1234" - task_id = "1234" + TASK_ID = "1234" task_name = "test task" responses.add( responses.GET, @@ -196,6 +187,7 @@ def test_submit(set_api_key): match=[ matchers.json_params_matcher( { + "taskType": TaskType.FDTD, "taskName": task_name, "callbackUrl": None, "fileType": "Gz", @@ -206,7 +198,7 @@ def test_submit(set_api_key): ], json={ "data": { - "taskId": task_id, + "taskId": TASK_ID, "taskName": task_name, "createdAt": "2022-01-01T00:00:00.000Z", } @@ -215,15 +207,19 @@ def test_submit(set_api_key): ) responses.add( responses.POST, - f"{Env.current.web_api_endpoint}/tidy3d/tasks/{task_id}/submit", + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/submit", match=[ matchers.json_params_matcher( - {"solverVersion": None, "workerGroup": None, "protocolVersion": __version__} + { + "protocolVersion": http_util.get_version(), + "solverVersion": None, + "workerGroup": None, + } ) ], json={ "data": { - "taskId": task_id, + "taskId": TASK_ID, "taskName": task_name, "createdAt": "2022-01-01T00:00:00.000Z", "taskBlockInfo": { @@ -238,9 +234,9 @@ def test_submit(set_api_key): ) responses.add( responses.GET, - f"{Env.current.web_api_endpoint}/tidy3d/tasks/{task_id}/detail", + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/detail", json={ - "taskId": task_id, + "taskId": TASK_ID, "taskName": task_name, "createdAt": "2022-01-01T00:00:00.000Z", "status": "running", @@ -253,17 +249,18 @@ def test_submit(set_api_key): }, status=200, ) - task = SimulationTask.create(None, task_name, "test folder1") + task = SimulationTask.create(TaskType.FDTD, task_name, "test folder1") task.submit() # test DE need to open the comment - # monitor(task_id, True) + # monitor(TASK_ID, True) @responses.activate def test_estimate_cost(set_api_key): + TASK_ID = "3eb06d16-208b-487b-864b-e9b1d3e010a7" responses.add( responses.GET, - f"{Env.current.web_api_endpoint}/tidy3d/tasks/3eb06d16-208b-487b-864b-e9b1d3e010a7/detail", + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/detail", json={ "data": { "taskId": "3eb06d16-208b-487b-864b-e9b1d3e010a7", @@ -281,11 +278,11 @@ def test_estimate_cost(set_api_key): responses.add( responses.POST, - f"{Env.current.web_api_endpoint}/tidy3d/tasks/3eb06d16-208b-487b-864b-e9b1d3e010a7/metadata", + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/metadata", json={"data": {"flexUnit": 2.33}}, status=200, ) - task = SimulationTask.get("3eb06d16-208b-487b-864b-e9b1d3e010a7") + task = SimulationTask(taskId=TASK_ID) assert task.estimate_cost()["flexUnit"] == 2.33 @@ -296,7 +293,7 @@ def mock(*args, **kwargs): with open(file_path, "w") as f: f.write("0.3,5.7") - monkeypatch.setattr("tidy3d.web.simulation_task.download_file", mock) + monkeypatch.setattr("tidy3d.web.core.task_core.download_file", mock) responses.add( responses.GET, f"{Env.current.web_api_endpoint}/tidy3d/tasks/3eb06d16-208b-487b-864b-e9b1d3e010a7/detail", diff --git a/tests/test_web/test_webapi.py b/tests/test_web/test_webapi.py index 2cfd6f0ad..c8fb17396 100644 --- a/tests/test_web/test_webapi.py +++ b/tests/test_web/test_webapi.py @@ -2,26 +2,42 @@ import pytest import responses +import numpy as np from _pytest import monkeypatch + import tidy3d as td from responses import matchers from tidy3d import Simulation from tidy3d.exceptions import SetupError -from tidy3d.web.environment import Env -from tidy3d.web.webapi import delete, delete_old, download, download_json, run, abort -from tidy3d.web.webapi import download_log, estimate_cost, get_info, get_run_info, get_tasks -from tidy3d.web.webapi import load, load_simulation, start, upload, monitor, real_cost -from tidy3d.web.container import Job, Batch -from tidy3d.web.asynchronous import run_async +from tidy3d.web.core.environment import Env +from tidy3d.web.api.webapi import delete, delete_old, download, download_json, run, abort +from tidy3d.web.api.webapi import download_log, estimate_cost, get_info, get_run_info, get_tasks +from tidy3d.web.api.webapi import load, load_simulation, start, upload, monitor, real_cost +from tidy3d.web.api.container import Job, Batch +from tidy3d.web.api.asynchronous import run_async from tidy3d.__main__ import main +from tidy3d.web.core.types import TaskType +from tidy3d.components.source import PointDipole, GaussianPulse +from tidy3d.components.grid.grid_spec import GridSpec +from tidy3d.components.data.sim_data import SimulationData +from tidy3d.components.data.monitor_data import FieldData +from tidy3d.components.data.data_array import ScalarFieldDataArray +from tidy3d.components.monitor import FieldMonitor TASK_NAME = "task_name_test" TASK_ID = "1234" +FOLDER_ID = "1234" CREATED_AT = "2022-01-01T00:00:00.000Z" PROJECT_NAME = "default" FLEX_UNIT = 1.0 EST_FLEX_UNIT = 11.11 +FILE_SIZE_GB = 4.0 + +task_core_path = "tidy3d.web.core.task_core" +api_path = "tidy3d.web.api.webapi" + +Env.dev.active() def make_sim(): @@ -36,32 +52,66 @@ def make_sim(): ) +def make_sim_data(file_size_gb=FILE_SIZE_GB): + """Makes a simulation.""" + # approximate # of points in the scalar field data + + N = int(2.528e8 / 4 * file_size_gb) + + n = int(N ** (0.25)) + + data = (1 + 1j) * np.random.random((n, n, n, n)) + x = np.linspace(-1, 1, n) + y = np.linspace(-1, 1, n) + z = np.linspace(-1, 1, n) + f = np.linspace(2e14, 4e14, n) + src = PointDipole( + center=(0, 0, 0), source_time=GaussianPulse(freq0=3e14, fwidth=1e14), polarization="Ex" + ) + coords = dict(x=x, y=y, z=z, f=f) + Ex = ScalarFieldDataArray(data, coords=coords) + monitor = FieldMonitor(size=(2, 2, 2), freqs=f, name="test", fields=["Ex"]) + field_data = FieldData(monitor=monitor, Ex=Ex) + sim = Simulation( + size=(2, 2, 2), + grid_spec=GridSpec(wavelength=1), + monitors=(monitor,), + sources=(src,), + run_time=1e-12, + ) + return SimulationData( + simulation=sim, + data=(field_data,), + ) + + @pytest.fixture def set_api_key(monkeypatch): """Set the api key.""" - import tidy3d.web.http_management as http_module + import tidy3d.web.core.http_util as http_module monkeypatch.setattr(http_module, "api_key", lambda: "apikey") + monkeypatch.setattr(http_module, "get_version", lambda: td.version.__version__) @pytest.fixture def mock_upload(monkeypatch, set_api_key): """Mocks webapi.upload.""" - responses.add( responses.GET, f"{Env.current.web_api_endpoint}/tidy3d/project", match=[matchers.query_param_matcher({"projectName": PROJECT_NAME})], - json={"data": {"projectId": TASK_ID, "projectName": PROJECT_NAME}}, + json={"data": {"projectId": FOLDER_ID, "projectName": PROJECT_NAME}}, status=200, ) responses.add( responses.POST, - f"{Env.current.web_api_endpoint}/tidy3d/projects/{TASK_ID}/tasks", + f"{Env.current.web_api_endpoint}/tidy3d/projects/{FOLDER_ID}/tasks", match=[ matchers.json_params_matcher( { + "taskType": TaskType.FDTD.name, "taskName": TASK_NAME, "callbackUrl": None, "simulationType": "tidy3d", @@ -83,7 +133,7 @@ def mock_upload(monkeypatch, set_api_key): def mock_download(*args, **kwargs): pass - monkeypatch.setattr("tidy3d.web.simulation_task.upload_file", mock_download) + monkeypatch.setattr("tidy3d.web.core.task_core.upload_file", mock_download) @pytest.fixture @@ -100,6 +150,7 @@ def mock_get_info(monkeypatch, set_api_key): "createdAt": CREATED_AT, "realFlexUnit": FLEX_UNIT, "estFlexUnit": EST_FLEX_UNIT, + "taskType": TaskType.FDTD.name, "metadataStatus": "processed", "status": "success", "s3Storage": 1.0, @@ -159,10 +210,10 @@ def mock_get_run_info(task_id): run_count[0] += 1 return perc_done, 1 - monkeypatch.setattr("tidy3d.web.webapi.REFRESH_TIME", 0.00001) - monkeypatch.setattr("tidy3d.web.webapi.RUN_REFRESH_TIME", 0.00001) - monkeypatch.setattr("tidy3d.web.webapi.get_status", mock_get_status) - monkeypatch.setattr("tidy3d.web.webapi.get_run_info", mock_get_run_info) + monkeypatch.setattr("tidy3d.web.api.connect_util.REFRESH_TIME", 0.00001) + monkeypatch.setattr(f"{api_path}.RUN_REFRESH_TIME", 0.00001) + monkeypatch.setattr(f"{api_path}.get_status", mock_get_status) + monkeypatch.setattr(f"{api_path}.get_run_info", mock_get_run_info) @pytest.fixture @@ -174,7 +225,7 @@ def _mock_download(*args, **kwargs): with open(file_path, "w") as f: f.write("0.3,5.7") - monkeypatch.setattr("tidy3d.web.simulation_task.download_file", _mock_download) + monkeypatch.setattr(f"{task_core_path}.download_file", _mock_download) download(TASK_ID, str(tmp_path / "web_test_tmp.json")) with open(str(tmp_path / "web_test_tmp.json")) as f: assert f.read() == "0.3,5.7" @@ -187,7 +238,7 @@ def mock_load(monkeypatch, set_api_key, mock_get_info): def _mock_download(*args, **kwargs): pass - monkeypatch.setattr("tidy3d.web.simulation_task.download_file", _mock_download) + monkeypatch.setattr(f"{task_core_path}.download_file", _mock_download) @pytest.fixture @@ -269,7 +320,7 @@ def _test_load(mock_load, mock_get_info, tmp_path): def mock_download(*args, **kwargs): pass - monkeypatch.setattr("tidy3d.web.simulation_task.download_file", mock_download) + monkeypatch.setattr(f"{task_core_path}.download_file", mock_download) load(TASK_ID, str(tmp_path / "monitor_data.hdf5")) @@ -305,8 +356,8 @@ def mock_download(*args, **kwargs): def get_str(*args, **kwargs): return sim.json().encode("utf-8") - monkeypatch.setattr("tidy3d.web.simulation_task.download_file", mock_download) - monkeypatch.setattr("tidy3d.web.simulation_task._read_simulation_from_hdf5", get_str) + monkeypatch.setattr(f"{task_core_path}.download_file", mock_download) + monkeypatch.setattr(f"{task_core_path}.read_simulation_from_hdf5", get_str) fname_tmp = str(tmp_path / "web_test_tmp.json") download_json(TASK_ID, fname_tmp) @@ -318,9 +369,7 @@ def test_load_simulation(monkeypatch, mock_get_info, tmp_path): def mock_download(*args, **kwargs): make_sim().to_file(args[1]) - monkeypatch.setattr( - "tidy3d.web.simulation_task.SimulationTask.get_simulation_json", mock_download - ) + monkeypatch.setattr(f"{task_core_path}.SimulationTask.get_simulation_json", mock_download) assert load_simulation(TASK_ID, str(tmp_path / "web_test_tmp.json")) @@ -332,7 +381,7 @@ def mock(*args, **kwargs): with open(file_path, "w") as f: f.write("0.3,5.7") - monkeypatch.setattr("tidy3d.web.simulation_task.download_file", mock) + monkeypatch.setattr(f"{task_core_path}.download_file", mock) download_log(TASK_ID, str(tmp_path / "web_test_tmp.json")) with open(str(tmp_path / "web_test_tmp.json")) as f: @@ -392,9 +441,12 @@ def test_get_tasks(set_api_key): @responses.activate def test_run(mock_webapi, monkeypatch, tmp_path): sim = make_sim() - monkeypatch.setattr("tidy3d.web.webapi.load", lambda *args, **kwargs: True) + monkeypatch.setattr(f"{api_path}.load", lambda *args, **kwargs: True) assert run( - sim, task_name=TASK_NAME, folder_name=PROJECT_NAME, path=str(tmp_path / "web_test_tmp.json") + sim, + task_name=TASK_NAME, + folder_name=PROJECT_NAME, + path=str(tmp_path / "web_test_tmp.json"), ) @@ -418,13 +470,32 @@ def test_abort_task(set_api_key): matchers.json_params_matcher( { "taskId": TASK_ID, - "taskType": "FDTD", + "taskType": TaskType.FDTD.name, } ) ], json={"result": True}, status=200, ) + responses.add( + responses.GET, + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/detail", + json={ + "taskId": TASK_ID, + "taskName": TASK_NAME, + "createdAt": "2022-01-01T00:00:00.000Z", + "status": "running", + "taskType": TaskType.FDTD.name, + "taskBlockInfo": { + "chargeType": "free", + "maxFreeCount": 20, + "maxGridPoints": 1000, + "maxTimeSteps": 1000, + }, + }, + status=200, + ) + abort(TASK_ID) @@ -433,7 +504,7 @@ def test_abort_task(set_api_key): @responses.activate def test_job(mock_webapi, monkeypatch, tmp_path): - monkeypatch.setattr("tidy3d.web.container.Job.load", lambda *args, **kwargs: True) + monkeypatch.setattr("tidy3d.web.api.container.Job.load", lambda *args, **kwargs: True) sim = make_sim() j = Job(simulation=sim, task_name=TASK_NAME, folder_name=PROJECT_NAME) @@ -447,14 +518,14 @@ def test_job(mock_webapi, monkeypatch, tmp_path): @pytest.fixture def mock_job_status(monkeypatch): - monkeypatch.setattr("tidy3d.web.container.Job.status", property(lambda self: "success")) - monkeypatch.setattr("tidy3d.web.container.Job.load", lambda *args, **kwargs: True) + monkeypatch.setattr("tidy3d.web.api.container.Job.status", property(lambda self: "success")) + monkeypatch.setattr("tidy3d.web.api.container.Job.load", lambda *args, **kwargs: True) @responses.activate def test_batch(mock_webapi, mock_job_status, tmp_path): - # monkeypatch.setattr("tidy3d.web.container.Batch.monitor", lambda self: time.sleep(0.1)) - # monkeypatch.setattr("tidy3d.web.container.Job.status", property(lambda self: "success")) + # monkeypatch.setattr("tidy3d.web.api.container.Batch.monitor", lambda self: time.sleep(0.1)) + # monkeypatch.setattr("tidy3d.web.api.container.Job.status", property(lambda self: "success")) sims = {TASK_NAME: make_sim()} b = Batch(simulations=sims, folder_name=PROJECT_NAME) @@ -468,7 +539,7 @@ def test_batch(mock_webapi, mock_job_status, tmp_path): @responses.activate def test_async(mock_webapi, mock_job_status): - # monkeypatch.setattr("tidy3d.web.container.Job.status", property(lambda self: "success")) + # monkeypatch.setattr("tidy3d.web.api.container.Job.status", property(lambda self: "success")) sims = {TASK_NAME: make_sim()} _ = run_async(sims, folder_name=PROJECT_NAME) diff --git a/tests/test_web/test_webapi_heat.py b/tests/test_web/test_webapi_heat.py new file mode 100644 index 000000000..b1dff1f7a --- /dev/null +++ b/tests/test_web/test_webapi_heat.py @@ -0,0 +1,350 @@ +# Tests webapi and things that depend on it + +import pytest +import responses +from _pytest import monkeypatch + +import tidy3d as td +from responses import matchers +from tidy3d import HeatSimulation +from tidy3d.web.core.environment import Env +from tidy3d.web.api.webapi import download, download_json, run, abort +from tidy3d.web.api.webapi import estimate_cost, get_info, get_run_info +from tidy3d.web.api.webapi import load_simulation, upload +from tidy3d.web.api.container import Job, Batch +from tidy3d.web.api.asynchronous import run_async + +from tidy3d.web.core.types import TaskType +from ..test_components.test_heat import make_heat_sim + +TASK_NAME = "task_name_test" +TASK_ID = "1234" +FOLDER_ID = "1234" +CREATED_AT = "2022-01-01T00:00:00.000Z" +PROJECT_NAME = "default" +FLEX_UNIT = 1.0 +EST_FLEX_UNIT = 11.11 +FILE_SIZE_GB = 4.0 + +task_core_path = "tidy3d.web.core.task_core" +api_path = "tidy3d.web.api.webapi" + + +@pytest.fixture +def set_api_key(monkeypatch): + """Set the api key.""" + import tidy3d.web.core.http_util as http_module + + monkeypatch.setattr(http_module, "api_key", lambda: "apikey") + monkeypatch.setattr(http_module, "get_version", lambda: td.version.__version__) + + +@pytest.fixture +def mock_upload(monkeypatch, set_api_key): + """Mocks webapi.upload.""" + responses.add( + responses.GET, + f"{Env.current.web_api_endpoint}/tidy3d/project", + match=[matchers.query_param_matcher({"projectName": PROJECT_NAME})], + json={"data": {"projectId": FOLDER_ID, "projectName": PROJECT_NAME}}, + status=200, + ) + + responses.add( + responses.POST, + f"{Env.current.web_api_endpoint}/tidy3d/projects/{FOLDER_ID}/tasks", + match=[ + matchers.json_params_matcher( + { + "taskType": TaskType.HEAT.name, + "taskName": TASK_NAME, + "callbackUrl": None, + "simulationType": "tidy3d", + "parentTasks": None, + "fileType": "Gz", + } + ) + ], + json={ + "data": { + "taskId": TASK_ID, + "taskName": TASK_NAME, + "createdAt": CREATED_AT, + } + }, + status=200, + ) + + def mock_download(*args, **kwargs): + pass + + monkeypatch.setattr("tidy3d.web.core.task_core.upload_file", mock_download) + + +@pytest.fixture +def mock_get_info(monkeypatch, set_api_key): + """Mocks webapi.get_info.""" + + responses.add( + responses.GET, + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/detail", + json={ + "data": { + "taskId": TASK_ID, + "taskName": TASK_NAME, + "taskType": TaskType.HEAT.name, + "createdAt": CREATED_AT, + "realFlexUnit": FLEX_UNIT, + "estFlexUnit": EST_FLEX_UNIT, + "metadataStatus": "processed", + "status": "success", + "s3Storage": 1.0, + } + }, + status=200, + ) + + +@pytest.fixture +def mock_start(monkeypatch, set_api_key, mock_get_info): + """Mocks webapi.start.""" + + responses.add( + responses.POST, + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/submit", + match=[ + matchers.json_params_matcher( + { + "solverVersion": None, + "workerGroup": None, + "protocolVersion": td.version.__version__, + } + ) + ], + json={ + "data": { + "taskId": TASK_ID, + "taskName": TASK_NAME, + "createdAt": CREATED_AT, + } + }, + status=200, + ) + + +@pytest.fixture +def mock_monitor(monkeypatch): + status_count = [0] + statuses = ("upload", "running", "running", "running", "running", "running", "success") + + def mock_get_status(task_id): + current_count = min(status_count[0], len(statuses) - 1) + current_status = statuses[current_count] + status_count[0] += 1 + return current_status + # return TaskInfo( + # status=current_status, taskName=TASK_NAME, taskId=task_id, realFlexUnit=1.0 + # ) + + run_count = [0] + perc_dones = (1, 10, 20, 30, 100) + + def mock_get_run_info(task_id): + current_count = min(run_count[0], len(perc_dones) - 1) + perc_done = perc_dones[current_count] + run_count[0] += 1 + return perc_done, 1 + + monkeypatch.setattr("tidy3d.web.api.connect_util.REFRESH_TIME", 0.00001) + monkeypatch.setattr(f"{api_path}.RUN_REFRESH_TIME", 0.00001) + monkeypatch.setattr(f"{api_path}.get_status", mock_get_status) + monkeypatch.setattr(f"{api_path}.get_run_info", mock_get_run_info) + + +@pytest.fixture +def mock_download(monkeypatch, set_api_key, mock_get_info, tmp_path): + """Mocks webapi.download.""" + + def _mock_download(*args, **kwargs): + file_path = kwargs["to_file"] + with open(file_path, "w") as f: + f.write("0.3,5.7") + + monkeypatch.setattr(f"{task_core_path}.download_file", _mock_download) + download(TASK_ID, str(tmp_path / "web_test_tmp.json")) + with open(str(tmp_path / "web_test_tmp.json"), "r") as f: + assert f.read() == "0.3,5.7" + + +@pytest.fixture +def mock_load(monkeypatch, set_api_key, mock_get_info): + """Mocks webapi.load""" + + def _mock_download(*args, **kwargs): + pass + + monkeypatch.setattr(f"{task_core_path}.download_file", _mock_download) + + +@pytest.fixture +def mock_metadata(monkeypatch, set_api_key): + """Mocks call to metadata api""" + responses.add( + responses.POST, + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/metadata", + json={ + "data": { + "createdAt": CREATED_AT, + } + }, + status=200, + ) + + +@pytest.fixture +def mock_get_run_info(monkeypatch, set_api_key): + """Mocks webapi.get_run_info""" + responses.add( + responses.GET, + f"{Env.current.web_api_endpoint}/tidy3d/tasks/{TASK_ID}/progress", + json={ + "data": { + "perc_done": 100, + "field_decay": 0, + } + }, + status=200, + ) + + +@pytest.fixture +def mock_webapi( + mock_upload, mock_metadata, mock_get_info, mock_start, mock_monitor, mock_download, mock_load +): + """Mocks all webapi operation.""" + + +@responses.activate +def test_upload(mock_upload): + sim = make_heat_sim() + assert upload(sim, TASK_NAME, PROJECT_NAME) + + +@responses.activate +def test_get_info(mock_get_info): + assert get_info(TASK_ID).taskId == TASK_ID + assert get_info(TASK_ID).taskType == "HEAT" + + +@responses.activate +def test_get_run_info(mock_get_run_info): + assert get_run_info(TASK_ID) == (100, 0) + + +@responses.activate +def test_estimate_cost(set_api_key, mock_get_info, mock_metadata): + assert estimate_cost(TASK_ID) == EST_FLEX_UNIT + + +@responses.activate +def test_download_json(monkeypatch, mock_get_info, tmp_path): + sim = make_heat_sim() + + def mock_download(*args, **kwargs): + pass + + def get_str(*args, **kwargs): + return sim.json().encode("utf-8") + + monkeypatch.setattr(f"{task_core_path}.download_file", mock_download) + monkeypatch.setattr(f"{task_core_path}.read_simulation_from_hdf5", get_str) + + fname_tmp = str(tmp_path / "web_test_tmp.json") + download_json(TASK_ID, fname_tmp) + assert HeatSimulation.from_file(fname_tmp) == sim + + +@responses.activate +def test_load_simulation(monkeypatch, mock_get_info, tmp_path): + def mock_download(*args, **kwargs): + make_heat_sim().to_file(args[1]) + + monkeypatch.setattr(f"{task_core_path}.SimulationTask.get_simulation_json", mock_download) + + assert load_simulation(TASK_ID, str(tmp_path / "web_test_tmp.json")) + + +@responses.activate +def test_run(mock_webapi, monkeypatch, tmp_path): + sim = make_heat_sim() + monkeypatch.setattr(f"{api_path}.load", lambda *args, **kwargs: True) + assert run( + sim, + task_name=TASK_NAME, + folder_name=PROJECT_NAME, + path=str(tmp_path / "web_test_tmp.json"), + ) + + +@responses.activate +def test_abort_task(set_api_key, mock_get_info): + responses.add( + responses.PUT, + f"{Env.current.web_api_endpoint}/tidy3d/tasks/abort", + match=[ + matchers.json_params_matcher( + { + "taskId": TASK_ID, + "taskType": TaskType.HEAT.name, + } + ) + ], + json={"result": True}, + status=200, + ) + abort(TASK_ID) + + +""" Containers """ + + +@responses.activate +def test_job(mock_webapi, monkeypatch, tmp_path): + monkeypatch.setattr("tidy3d.web.api.container.Job.load", lambda *args, **kwargs: True) + sim = make_heat_sim() + j = Job(simulation=sim, task_name=TASK_NAME, folder_name=PROJECT_NAME) + + _ = j.run(path=str(tmp_path / "web_test_tmp.json")) + _ = j.status + j.estimate_cost() + # j.download + _ = j.delete + assert j.real_cost() == FLEX_UNIT + + +@pytest.fixture +def mock_job_status(monkeypatch): + monkeypatch.setattr("tidy3d.web.api.container.Job.status", property(lambda self: "success")) + monkeypatch.setattr("tidy3d.web.api.container.Job.load", lambda *args, **kwargs: True) + + +@responses.activate +def test_batch(mock_webapi, mock_job_status, tmp_path): + # monkeypatch.setattr("tidy3d.web.api.container.Batch.monitor", lambda self: time.sleep(0.1)) + # monkeypatch.setattr("tidy3d.web.api.container.Job.status", property(lambda self: "success")) + + sims = {TASK_NAME: make_heat_sim()} + b = Batch(simulations=sims, folder_name=PROJECT_NAME) + b.estimate_cost() + _ = b.run(path_dir=str(tmp_path)) + assert b.real_cost() == FLEX_UNIT * len(sims) + + +""" Async """ + + +@responses.activate +def test_async(mock_webapi, mock_job_status): + # monkeypatch.setattr("tidy3d.web.api.container.Job.status", property(lambda self: "success")) + sims = {TASK_NAME: make_heat_sim()} + _ = run_async(sims, folder_name=PROJECT_NAME) diff --git a/tests/utils.py b/tests/utils.py index 8ac88176d..95a7be45c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -14,19 +14,20 @@ np.random.seed(4) +FREQS = np.array([1.90, 2.01, 2.2]) * 1e12 SIM_MONITORS = td.Simulation( size=(10.0, 10.0, 10.0), grid_spec=td.GridSpec(wavelength=1.0), run_time=1e-13, monitors=[ - td.FieldMonitor(size=(1, 1, 1), center=(0, 1, 0), freqs=[1, 2, 5, 7, 8], name="field_freq"), + td.FieldMonitor(size=(1, 1, 1), center=(0, 1, 0), freqs=FREQS, name="field_freq"), td.FieldTimeMonitor(size=(1, 1, 0), center=(1, 0, 0), interval=10, name="field_time"), - td.FluxMonitor(size=(1, 1, 0), center=(0, 0, 0), freqs=[1, 2, 5, 9], name="flux_freq"), + td.FluxMonitor(size=(1, 1, 0), center=(0, 0, 0), freqs=FREQS, name="flux_freq"), td.FluxTimeMonitor(size=(1, 1, 0), center=(0, 0, 0), start=1e-12, name="flux_time"), td.ModeMonitor( size=(1, 1, 0), center=(0, 0, 0), - freqs=[1.90, 2.01, 2.2], + freqs=FREQS, mode_spec=td.ModeSpec(num_modes=3), name="mode", ), @@ -102,19 +103,25 @@ structures=[ td.Structure( geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), - medium=td.Medium(permittivity=2.0), + medium=td.Medium(permittivity=2.0, name="dieletric"), + name="dieletric_box", ), td.Structure( geometry=td.Box(size=(1, td.inf, 1), center=(-1, 0, 0)), - medium=td.Medium(permittivity=1.0, conductivity=3.0), + medium=td.Medium(permittivity=1.0, conductivity=3.0, name="lossy_dieletric"), + name="lossy_box", ), td.Structure( geometry=td.Sphere(radius=1.0, center=(1.0, 0.0, 1.0)), - medium=td.Sellmeier(coeffs=[(1.03961212, 0.00600069867), (0.231792344, 0.0200179144)]), + medium=td.Sellmeier( + coeffs=[(1.03961212, 0.00600069867), (0.231792344, 0.0200179144)], name="sellmeier" + ), + name="sellmeier_sphere", ), td.Structure( geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), - medium=td.Lorentz(eps_inf=2.0, coeffs=[(1, 2, 3)]), + medium=td.Lorentz(eps_inf=2.0, coeffs=[(1, 2, 3)], name="lorentz"), + name="lorentz_box", ), td.Structure( geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), @@ -126,15 +133,25 @@ ), td.Structure( geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), - medium=td.Drude(eps_inf=2.0, coeffs=[(1, 3)]), + medium=td.Drude(eps_inf=2.0, coeffs=[(1, 3)], name="drude"), + name="drude_box", ), td.Structure( geometry=td.Box(size=(1, 0, 1), center=(-1, 0, 0)), medium=td.Medium2D.from_medium(td.Medium(conductivity=0.45), thickness=0.01), ), + td.Structure( + geometry=td.Box(size=(1, 0, 1), center=(-1, 0, 0)), + medium=td.PEC2D, + ), + td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), + medium=td.AnisotropicMedium(xx=td.PEC, yy=td.Medium(), zz=td.Medium()), + ), td.Structure( geometry=td.GeometryGroup(geometries=[td.Box(size=(1, 1, 1), center=(-1, 0, 0))]), medium=td.PEC, + name="pec_group", ), td.Structure( geometry=td.Cylinder(radius=1.0, length=2.0, center=(1.0, 0.0, -1.0), axis=1), @@ -143,6 +160,7 @@ yy=td.Medium(permittivity=2), zz=td.Medium(permittivity=3), ), + name="anisotopic_cylinder", ), td.Structure( geometry=td.PolySlab( @@ -151,6 +169,7 @@ medium=td.PoleResidue( eps_inf=1.0, poles=((6206417594288582j, (-3.311074436985222e16j)),) ), + name="pole_slab", ), td.Structure( geometry=td.Box( @@ -203,6 +222,22 @@ nonlinear_spec=td.NonlinearSusceptibility(chi3=0.1, numiters=20), ), ), + td.Structure( + geometry=td.Box( + size=(1, 1, 1), + center=(-1.0, 0.5, 0.5), + ), + medium=td.Medium( + nonlinear_spec=td.NonlinearSpec( + num_iters=10, + models=[ + td.NonlinearSusceptibility(chi3=0.1), + td.TwoPhotonAbsorption(beta=1), + td.KerrNonlinearity(n2=1), + ], + ) + ), + ), td.Structure( geometry=td.PolySlab( vertices=[(-1.5, -1.5), (-0.5, -1.5), (-0.5, -0.5)], slab_bounds=[-1, 1] @@ -231,6 +266,7 @@ ) ), medium=td.Medium(permittivity=5), + name="dieletric_mesh", ), td.Structure( geometry=td.TriangleMesh.from_stl( @@ -238,6 +274,23 @@ ), medium=td.Medium(permittivity=5), ), + td.Structure( + geometry=td.ClipOperation( + geometry_a=td.Box(size=(1, 1, 1), center=(0.9, 0.9, 0.9)), + geometry_b=td.Box(size=(1, 1, 1), center=(1.1, 1.1, 1.1)), + operation="symmetric_difference", + ), + medium=td.Medium(permittivity=3), + name="clip_operation", + ), + td.Structure( + geometry=td.Transformed( + geometry=td.Box(size=(1, 1, 1), center=(1, 1, 1)), + transform=td.Transformed.rotation(np.pi / 12, 2), + ), + medium=td.Medium(permittivity=1.5), + name="transformed_box", + ), ], sources=[ td.UniformCurrentSource( @@ -384,8 +437,8 @@ freqs=[250e12, 300e12], name="proj_angle", custom_origin=(1, 2, 3), - phi=[0, np.pi / 2], - theta=np.linspace(-np.pi / 2, np.pi / 2, 100), + phi=[0, np.pi / 6], + theta=np.linspace(np.pi / 4, np.pi / 4 + np.pi / 2, 100), ), td.FieldProjectionCartesianMonitor( center=(0, 0, 0), @@ -405,8 +458,8 @@ name="proj_kspace", custom_origin=(1, 2, 3), proj_axis=2, - ux=[0.1, 0.2], - uy=[0.3, 0.4, 0.5], + ux=[0.02, 0.04], + uy=[0.03, 0.04, 0.05], ), td.FieldProjectionAngleMonitor( center=(0, 0, 0), @@ -414,8 +467,8 @@ freqs=[250e12, 300e12], name="proj_angle_exact", custom_origin=(1, 2, 3), - phi=[0, np.pi / 2], - theta=np.linspace(-np.pi / 2, np.pi / 2, 100), + phi=[0, np.pi / 8], + theta=np.linspace(np.pi / 4, np.pi / 4 + np.pi / 2, 100), far_field_approx=False, ), td.DiffractionMonitor( diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 5e30aa4a2..23f072bfd 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -5,24 +5,29 @@ from .components.grid.grid_spec import GridSpec, UniformGrid, CustomGrid, AutoGrid # geometry -from .components.geometry.base import Box, ClipOperation, GeometryGroup +from .components.geometry.base import Box, Transformed, ClipOperation, GeometryGroup from .components.geometry.primitives import Sphere, Cylinder from .components.geometry.mesh import TriangleMesh from .components.geometry.polyslab import PolySlab # medium -from .components.medium import Medium, PoleResidue, AnisotropicMedium, PEC, PECMedium, Medium2D +from .components.medium import Medium, PoleResidue, AnisotropicMedium, PEC, PECMedium +from .components.medium import Medium2D, PEC2D from .components.medium import Sellmeier, Debye, Drude, Lorentz from .components.medium import CustomMedium, CustomPoleResidue from .components.medium import CustomSellmeier, FullyAnisotropicMedium from .components.medium import CustomLorentz, CustomDrude, CustomDebye, CustomAnisotropicMedium -from .components.medium import NonlinearSusceptibility +from .components.medium import NonlinearSusceptibility, TwoPhotonAbsorption, KerrNonlinearity from .components.transformation import RotationAroundAxis from .components.medium import PerturbationMedium, PerturbationPoleResidue from .components.parameter_perturbation import ParameterPerturbation from .components.parameter_perturbation import LinearHeatPerturbation, CustomHeatPerturbation from .components.parameter_perturbation import LinearChargePerturbation, CustomChargePerturbation +# time modulation +from .components.time_modulation import SpaceTimeModulation, SpaceModulation +from .components.time_modulation import ContinuousWaveTimeModulation, ModulationSpec + # structures from .components.structure import Structure, MeshOverrideStructure @@ -73,6 +78,8 @@ from .components.data.monitor_data import DiffractionData from .components.data.sim_data import SimulationData from .components.data.sim_data import DATA_TYPE_MAP +from .components.data.data_array import PointDataArray, CellDataArray, IndexedDataArray +from .components.data.dataset import TriangularGridDataset, TetrahedralGridDataset # boundary from .components.boundary import BoundarySpec, Boundary, BoundaryEdge, BoundaryEdgeType @@ -90,7 +97,7 @@ from .material_library.parametric_materials import Graphene # for docs -from .components.medium import AbstractMedium, NonlinearSpec +from .components.medium import AbstractMedium, NonlinearSpec, NonlinearModel from .components.geometry.base import Geometry from .components.source import Source, SourceTime from .components.monitor import Monitor @@ -107,6 +114,25 @@ # updater from .updater import Updater +# scene +from .components.scene import Scene + +# boundary placement for other solvers +from .components.bc_placement import StructureStructureInterface, StructureBoundary +from .components.bc_placement import MediumMediumInterface +from .components.bc_placement import StructureSimulationBoundary +from .components.bc_placement import SimulationBoundary + +# heat +from .components.heat_spec import FluidSpec, SolidSpec +from .components.heat.simulation import HeatSimulation +from .components.heat.data.sim_data import HeatSimulationData +from .components.heat.data.monitor_data import TemperatureData +from .components.heat.boundary import TemperatureBC, ConvectionBC, HeatFluxBC, HeatBoundarySpec +from .components.heat.source import UniformHeatSource +from .components.heat.monitor import TemperatureMonitor +from .components.heat.grid import UniformUnstructuredGrid, DistanceUnstructuredGrid + def set_logging_level(level: str) -> None: """Raise a warning here instead of setting the logging level.""" @@ -118,6 +144,7 @@ def set_logging_level(level: str) -> None: log.info(f"Using client version: {__version__}") +Transformed.update_forward_refs() ClipOperation.update_forward_refs() GeometryGroup.update_forward_refs() @@ -133,6 +160,8 @@ def set_logging_level(level: str) -> None: "Cylinder", "PolySlab", "GeometryGroup", + "ClipOperation", + "Transformed", "TriangleMesh", "Medium", "PoleResidue", @@ -140,6 +169,7 @@ def set_logging_level(level: str) -> None: "PEC", "PECMedium", "Medium2D", + "PEC2D", "Sellmeier", "Debye", "Drude", @@ -161,7 +191,10 @@ def set_logging_level(level: str) -> None: "LinearChargePerturbation", "CustomChargePerturbation", "NonlinearSpec", + "NonlinearModel", "NonlinearSusceptibility", + "TwoPhotonAbsorption", + "KerrNonlinearity", "Structure", "MeshOverrideStructure", "ModeSpec", @@ -265,4 +298,32 @@ def set_logging_level(level: str) -> None: "config", "__version__", "Updater", + "Scene", + "StructureStructureInterface", + "StructureBoundary", + "MediumMediumInterface", + "StructureSimulationBoundary", + "SimulationBoundary", + "FluidSpec", + "SolidSpec", + "HeatSimulation", + "HeatSimulationData", + "TemperatureBC", + "ConvectionBC", + "HeatFluxBC", + "HeatBoundarySpec", + "UniformHeatSource", + "UniformUnstructuredGrid", + "DistanceUnstructuredGrid", + "TemperatureData", + "TemperatureMonitor", + "SpaceTimeModulation", + "SpaceModulation", + "ContinuousWaveTimeModulation", + "ModulationSpec", + "PointDataArray", + "CellDataArray", + "IndexedDataArray", + "TriangularGridDataset", + "TetrahedralGridDataset", ] diff --git a/tidy3d/components/base.py b/tidy3d/components/base.py index bfca08e8d..6a61839a9 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -7,6 +7,9 @@ import tempfile from functools import wraps from typing import List, Callable, Dict, Union, Tuple, Any +from math import ceil +import io +import hashlib import rich import pydantic.v1 as pydantic @@ -22,9 +25,12 @@ from ..exceptions import FileError from ..log import log -# default indentation (# spaces) in files -INDENT = 4 + +INDENT_JSON_FILE = 4 # default indentation of json string in json files +INDENT = None # default indentation of json string used internally JSON_TAG = "JSON_STRING" +# If json string is larger than ``MAX_STRING_LENGTH``, split the string when storing in hdf5 +MAX_STRING_LENGTH = 1e9 def cache(prop): @@ -64,13 +70,20 @@ def ndarray_encoder(val): def _get_valid_extension(fname: str) -> str: """Return the file extension from fname, validated to accepted ones.""" - extension = "".join(pathlib.Path(fname).suffixes).lower() - valid_extensions = [".json", ".yaml", ".hdf5", ".hdf5.gz", ".h5"] - if extension not in valid_extensions: - raise FileError( - f"{fname}::File extension must be in {valid_extensions}, but '{extension}' given" - ) - return extension + valid_extensions = [".json", ".yaml", ".hdf5", ".h5", ".hdf5.gz"] + extensions = [s.lower() for s in pathlib.Path(fname).suffixes[-2:]] + if len(extensions) == 0: + raise FileError(f"File '{fname}' missing extension.") + single_extension = extensions[-1] + if single_extension in valid_extensions: + return single_extension + double_extension = "".join(extensions) + if double_extension in valid_extensions: + return double_extension + raise FileError( + f"File extension must be one of {', '.join(valid_extensions)}; file '{fname}' does not " + "match any of those." + ) class Tidy3dBaseModel(pydantic.BaseModel): @@ -88,6 +101,12 @@ def __hash__(self) -> int: except TypeError: return hash(self.json()) + def _hash_self(self) -> str: + """Hash this component with ``hashlib`` in a way that is the same every session.""" + bf = io.BytesIO() + self.to_hdf5(bf) + return hashlib.sha256(bf.getvalue()).hexdigest() + def __init__(self, **kwargs): """Init method, includes post-init validators.""" log.begin_capture() @@ -309,7 +328,7 @@ def to_json(self, fname: str) -> None: ------- >>> simulation.to_json(fname='folder/sim.json') # doctest: +SKIP """ - json_string = self._json_string + json_string = self._json(indent=INDENT_JSON_FILE) self._warn_if_contains_data(json_string) with open(fname, "w", encoding="utf-8") as file_handle: file_handle.write(json_string) @@ -375,7 +394,7 @@ def to_yaml(self, fname: str) -> None: self._warn_if_contains_data(json_string) model_dict = json.loads(json_string) with open(fname, "w+", encoding="utf-8") as file_handle: - yaml.dump(model_dict, file_handle, indent=INDENT) + yaml.dump(model_dict, file_handle, indent=INDENT_JSON_FILE) @staticmethod def _warn_if_contains_data(json_str: str) -> None: @@ -430,6 +449,23 @@ def get_sub_model(cls, group_path: str, model_dict: dict | list) -> dict: model_dict = model_dict[key] return model_dict + @staticmethod + def _json_string_key(index: int) -> str: + """Get json string key for string chunk number ``index``.""" + if index: + return f"{JSON_TAG}_{index}" + return JSON_TAG + + @classmethod + def _json_string_from_hdf5(cls, fname: str) -> str: + """Load the model json string from an hdf5 file.""" + with h5py.File(fname, "r") as f_handle: + num_string_parts = len([key for key in f_handle.keys() if JSON_TAG in key]) + json_string = b"" + for ind in range(num_string_parts): + json_string += f_handle[cls._json_string_key(ind)][()] + return json_string + @classmethod def dict_from_hdf5( cls, fname: str, group_path: str = "", custom_decoders: List[Callable] = None @@ -501,10 +537,7 @@ def load_data_from_file(model_dict: dict, group_path: str = "") -> None: elif isinstance(value, dict): load_data_from_file(model_dict=value, group_path=subpath) - with h5py.File(fname, "r") as f_handle: - json_string = f_handle[JSON_TAG][()] - model_dict = json.loads(json_string) - + model_dict = json.loads(cls._json_string_from_hdf5(fname=fname)) group_path = cls._construct_group_path(group_path) model_dict = cls.get_sub_model(group_path=group_path, model_dict=model_dict) load_data_from_file(model_dict=model_dict, group_path=group_path) @@ -563,7 +596,11 @@ def to_hdf5(self, fname: str, custom_encoders: List[Callable] = None) -> None: with h5py.File(fname, "w") as f_handle: - f_handle[JSON_TAG] = self._json_string + json_str = self._json_string + for ind in range(ceil(len(json_str) / MAX_STRING_LENGTH)): + ind_start = int(ind * MAX_STRING_LENGTH) + ind_stop = min(int(ind + 1) * MAX_STRING_LENGTH, len(json_str)) + f_handle[self._json_string_key(ind)] = json_str[ind_start:ind_stop] def add_data_to_file(data_dict: dict, group_path: str = "") -> None: """For every DataArray item in dictionary, write path of hdf5 group as value.""" @@ -707,7 +744,51 @@ def __ge__(self, other): def __eq__(self, other): """Define == for two Tidy3DBaseModels.""" - return self._json_string == other._json_string + if other is None: + return False + + def check_equal(dict1: dict, dict2: dict) -> bool: + """Check if two dictionaries are equal, with special handlings.""" + + # if different keys, automatically fail + if not dict1.keys() == dict2.keys(): + return False + + # loop through elements in each dict + for key in dict1.keys(): + val1 = dict1[key] + val2 = dict2[key] + + # if one of val1 or val2 is None (exclusive OR) + if (val1 is None) != (val2 is None): + return False + + # convert tuple to dict to use this recursive function + if isinstance(val1, tuple) or isinstance(val2, tuple): + val1 = dict(zip(range(len(val1)), val1)) + val2 = dict(zip(range(len(val2)), val2)) + + # if dictionaries, recurse + if isinstance(val1, dict) or isinstance(val2, dict): + are_equal = check_equal(val1, val2) + if not are_equal: + return False + + # if numpy arrays, use numpy to do equality check + elif isinstance(val1, np.ndarray) or isinstance(val2, np.ndarray): + if not np.array_equal(val1, val2): + return False + + # everything else + else: + + # note: this logic is because != is handled differently in DataArrays apparently + if not val1 == val2: + return False + + return True + + return check_equal(self.dict(), other.dict()) @cached_property def _json_string(self) -> str: diff --git a/tidy3d/components/base_sim/__init__.py b/tidy3d/components/base_sim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tidy3d/components/base_sim/data/__init__.py b/tidy3d/components/base_sim/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tidy3d/components/base_sim/data/monitor_data.py b/tidy3d/components/base_sim/data/monitor_data.py new file mode 100644 index 000000000..fffcf130c --- /dev/null +++ b/tidy3d/components/base_sim/data/monitor_data.py @@ -0,0 +1,25 @@ +"""Abstract base for monitor data structures.""" +from __future__ import annotations +from abc import ABC + +import pydantic.v1 as pd + +from ..monitor import AbstractMonitor +from ...data.dataset import Dataset + + +class AbstractMonitorData(Dataset, ABC): + """Abstract base class of objects that store data pertaining to a single + :class:`AbstractMonitor`. + """ + + monitor: AbstractMonitor = pd.Field( + ..., + title="Monitor", + description="Monitor associated with the data.", + ) + + @property + def symmetry_expanded_copy(self) -> AbstractMonitorData: + """Return copy of self with symmetry applied.""" + return self.copy() diff --git a/tidy3d/components/base_sim/data/sim_data.py b/tidy3d/components/base_sim/data/sim_data.py new file mode 100644 index 000000000..1707ebc7d --- /dev/null +++ b/tidy3d/components/base_sim/data/sim_data.py @@ -0,0 +1,126 @@ +"""Abstract base for simulation data structures.""" +from __future__ import annotations +from typing import Dict, Tuple, Union + +from abc import ABC + +import xarray as xr +import pydantic.v1 as pd +import numpy as np + +from .monitor_data import AbstractMonitorData +from ..simulation import AbstractSimulation +from ...data.dataset import UnstructuredGridDatasetType +from ...base import Tidy3dBaseModel +from ...types import FieldVal +from ....exceptions import DataError, Tidy3dKeyError, ValidationError + + +class AbstractSimulationData(Tidy3dBaseModel, ABC): + """Stores data from a collection of :class:`AbstractMonitor` objects in + a :class:`AbstractSimulation`. + """ + + simulation: AbstractSimulation = pd.Field( + ..., + title="Simulation", + description="Original :class:`AbstractSimulation` associated with the data.", + ) + + data: Tuple[AbstractMonitorData, ...] = pd.Field( + ..., + title="Monitor Data", + description="List of :class:`AbstractMonitorData` instances " + "associated with the monitors of the original :class:`AbstractSimulation`.", + ) + + log: str = pd.Field( + None, + title="Solver Log", + description="A string containing the log information from the simulation run.", + ) + + def __getitem__(self, monitor_name: str) -> AbstractMonitorData: + """Get a :class:`.AbstractMonitorData` by name. Apply symmetry if applicable.""" + monitor_data = self.monitor_data[monitor_name] + return monitor_data.symmetry_expanded_copy + + @property + def monitor_data(self) -> Dict[str, AbstractMonitorData]: + """Dictionary mapping monitor name to its associated :class:`AbstractMonitorData`.""" + return {monitor_data.monitor.name: monitor_data for monitor_data in self.data} + + @pd.validator("data", always=True) + def data_monitors_match_sim(cls, val, values): + """Ensure each :class:`AbstractMonitorData` in ``.data`` corresponds to a monitor in + ``.simulation``. + """ + sim = values.get("simulation") + if sim is None: + raise ValidationError("'.simulation' failed validation, can't validate data.") + for mnt_data in val: + try: + monitor_name = mnt_data.monitor.name + sim.get_monitor_by_name(monitor_name) + except Tidy3dKeyError as exc: + raise DataError( + f"Data with monitor name '{monitor_name}' supplied " + f"but not found in the original '{sim.type}'." + ) from exc + return val + + @pd.validator("data", always=True) + def validate_no_ambiguity(cls, val, values): + """Ensure all :class:`AbstractMonitorData` entries in ``.data`` correspond to different + monitors in ``.simulation``. + """ + sim = values.get("simulation") + if sim is None: + raise ValidationError("'.simulation' failed validation, can't validate data.") + + names = [mnt_data.monitor.name for mnt_data in val] + + if len(set(names)) != len(names): + raise ValidationError("Some entries of '.data' provide data for same monitor(s).") + + return val + + @staticmethod + def _field_component_value( + field_component: Union[xr.DataArray, UnstructuredGridDatasetType], val: FieldVal + ) -> xr.DataArray: + """return the desired value of a field component. + + Parameter + ---------- + field_component : Union[xarray.DataArray, UnstructuredGridDatasetType] + Field component from which to calculate the value. + val : Literal['real', 'imag', 'abs', 'abs^2', 'phase'] + Which part of the field to return. + + Returns + ------- + xarray.DataArray + Value extracted from the field component. + """ + if val == "real": + field_value = field_component.real + field_value = field_value.rename(f"Re{{{field_component.name}}}") + + elif val == "imag": + field_value = field_component.imag + field_value = field_value.rename(f"Im{{{field_component.name}}}") + + elif val == "abs": + field_value = np.abs(field_component) + field_value = field_value.rename(f"|{field_component.name}|") + + elif val == "abs^2": + field_value = np.abs(field_component) ** 2 + field_value = field_value.rename(f"|{field_component.name}|²") + + elif val == "phase": + field_value = np.arctan2(field_component.imag, field_component.real) + field_value = field_value.rename(f"∠{field_component.name}") + + return field_value diff --git a/tidy3d/components/base_sim/monitor.py b/tidy3d/components/base_sim/monitor.py new file mode 100644 index 000000000..ff13887fd --- /dev/null +++ b/tidy3d/components/base_sim/monitor.py @@ -0,0 +1,92 @@ +"""Abstract bases for classes that define how data is recorded from simulation.""" +from abc import ABC, abstractmethod +from typing import Tuple + +import pydantic.v1 as pd +import numpy as np + +from ..types import ArrayFloat1D, Numpy +from ..types import Axis +from ..geometry.base import Box +from ..base import cached_property +from ..viz import PlotParams, plot_params_monitor + + +class AbstractMonitor(Box, ABC): + """Abstract base class for steady-state monitors.""" + + name: str = pd.Field( + ..., + title="Name", + description="Unique name for monitor.", + min_length=1, + ) + + @cached_property + def plot_params(self) -> PlotParams: + """Default parameters for plotting a Monitor object.""" + return plot_params_monitor + + @cached_property + def geometry(self) -> Box: + """:class:`Box` representation of monitor. + + Returns + ------- + :class:`Box` + Representation of the monitor geometry as a :class:`Box`. + """ + return Box(center=self.center, size=self.size) + + @abstractmethod + def storage_size(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Size of monitor storage given the number of points after discretization. + + Parameters + ---------- + num_cells : int + Number of grid cells within the monitor after discretization by a :class:`Simulation`. + tmesh : Array + The discretized time mesh of a :class:`Simulation`. + + Returns + ------- + int + Number of bytes to be stored in monitor. + """ + + def downsample(self, arr: Numpy, axis: Axis) -> Numpy: + """Downsample a 1D array making sure to keep the first and last entries, based on the + spatial interval defined for the ``axis``. + + Parameters + ---------- + arr : Numpy + A 1D array of arbitrary type. + axis : Axis + Axis for which to select the interval_space defined for the monitor. + + Returns + ------- + Numpy + Downsampled array. + """ + + size = len(arr) + interval = self.interval_space[axis] + # There should always be at least 3 indices for "surface" monitors. Also, if the + # size along this dim is already smaller than the interval, then don't downsample. + if size < 4 or (size - 1) <= interval: + return arr + # make sure the last index is always included + inds = np.arange(0, size, interval) + if inds[-1] != size - 1: + inds = np.append(inds, size - 1) + return arr[inds] + + def downsampled_num_cells(self, num_cells: Tuple[int, int, int]) -> Tuple[int, int, int]: + """Given a tuple of the number of cells spanned by the monitor along each dimension, + return the number of cells one would have after downsampling based on ``interval_space``. + """ + arrs = [np.arange(ncells) for ncells in num_cells] + return tuple((self.downsample(arr, axis=dim).size for dim, arr in enumerate(arrs))) diff --git a/tidy3d/components/base_sim/simulation.py b/tidy3d/components/base_sim/simulation.py new file mode 100644 index 000000000..7988f14b3 --- /dev/null +++ b/tidy3d/components/base_sim/simulation.py @@ -0,0 +1,608 @@ +"""Abstract base for defining simulation classes of different solvers""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Tuple +from math import isclose + +import pydantic.v1 as pd + +from .monitor import AbstractMonitor + +from ..base import cached_property +from ..validators import assert_unique_names, assert_objects_in_sim_bounds +from ..geometry.base import Box +from ..types import Ax, Bound, Axis, Symmetry, TYPE_TAG_STR +from ..structure import Structure +from ..viz import add_ax_if_none, equal_aspect +from ..scene import Scene + +from ..medium import Medium, MediumType3D + +from ..viz import PlotParams, plot_params_symmetry + +from ...version import __version__ +from ...exceptions import Tidy3dKeyError +from ...log import log + + +class AbstractSimulation(Box, ABC): + """Base class for simulation classes of different solvers.""" + + medium: MediumType3D = pd.Field( + Medium(), + title="Background Medium", + description="Background medium of simulation, defaults to vacuum if not specified.", + discriminator=TYPE_TAG_STR, + ) + + structures: Tuple[Structure, ...] = pd.Field( + (), + title="Structures", + description="Tuple of structures present in simulation. " + "Note: Structures defined later in this list override the " + "simulation material properties in regions of spatial overlap.", + ) + + symmetry: Tuple[Symmetry, Symmetry, Symmetry] = pd.Field( + (0, 0, 0), + title="Symmetries", + description="Tuple of integers defining reflection symmetry across a plane " + "bisecting the simulation domain normal to the x-, y-, and z-axis " + "at the simulation center of each 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.", + ) + + sources: Tuple[None, ...] = pd.Field( + (), + title="Sources", + description="Sources in the simulation.", + ) + + boundary_spec: None = pd.Field( + None, + title="Boundaries", + description="Specification of boundary conditions.", + ) + + monitors: Tuple[None, ...] = pd.Field( + (), + title="Monitors", + description="Monitors in the simulation. ", + ) + + grid_spec: None = pd.Field( + None, + title="Grid Specification", + description="Specifications for the simulation grid.", + ) + + version: str = pd.Field( + __version__, + title="Version", + description="String specifying the front end version number.", + ) + + """ Validating setup """ + + # make sure all names are unique + _unique_monitor_names = assert_unique_names("monitors") + _unique_structure_names = assert_unique_names("structures") + _unique_source_names = assert_unique_names("sources") + + _monitors_in_bounds = assert_objects_in_sim_bounds("monitors") + _structures_in_bounds = assert_objects_in_sim_bounds("structures", error=False) + + @pd.validator("structures", always=True) + def _structures_not_at_edges(cls, val, values): + """Warn if any structures lie at the simulation boundaries.""" + + if val is None: + return val + + sim_box = Box(size=values.get("size"), center=values.get("center")) + sim_bound_min, sim_bound_max = sim_box.bounds + sim_bounds = list(sim_bound_min) + list(sim_bound_max) + + with log as consolidated_logger: + for istruct, structure in enumerate(val): + struct_bound_min, struct_bound_max = structure.geometry.bounds + struct_bounds = list(struct_bound_min) + list(struct_bound_max) + + for sim_val, struct_val in zip(sim_bounds, struct_bounds): + + if isclose(sim_val, struct_val): + consolidated_logger.warning( + f"Structure at 'structures[{istruct}]' has bounds that extend exactly " + "to simulation edges. This can cause unexpected behavior. " + "If intending to extend the structure to infinity along one dimension, " + "use td.inf as a size variable instead to make this explicit.", + custom_loc=["structures", istruct], + ) + + return val + + """ Post-init validators """ + + def _post_init_validators(self) -> None: + """Call validators taking z`self` that get run after init.""" + _ = self.scene + + """ Accounting """ + + @cached_property + def scene(self) -> Scene: + """Scene instance associated with the simulation.""" + + return Scene(medium=self.medium, structures=self.structures) + + def get_monitor_by_name(self, name: str) -> AbstractMonitor: + """Return monitor named 'name'.""" + for monitor in self.monitors: + if monitor.name == name: + return monitor + raise Tidy3dKeyError(f"No monitor named '{name}'") + + @cached_property + def simulation_bounds(self) -> Bound: + """Simulation bounds including auxiliary boundary zones such as PML layers.""" + # in this default implementation we just take self.bounds + # this should be changed in different solvers depending on whether automatic extensions + # (like pml) are present + return self.bounds + + @cached_property + def simulation_geometry(self) -> Box: + """The entire simulation domain including auxiliary boundary zones such as PML layers. + It is identical to ``Simulation.geometry`` in the absence of such auxiliary zones. + """ + rmin, rmax = self.simulation_bounds + return Box.from_bounds(rmin=rmin, rmax=rmax) + + @cached_property + def simulation_structure(self) -> Structure: + """Returns structure representing the domain of the simulation. This differs from + ``Simulation.scene.background_structure`` in that it has finite extent.""" + return Structure(geometry=self.simulation_geometry, medium=self.medium) + + @equal_aspect + @add_ax_if_none + def plot( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + source_alpha: float = None, + monitor_alpha: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + **patch_kwargs, + ) -> Ax: + """Plot each of simulation's components 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. + source_alpha : float = None + Opacity of the sources. If ``None``, uses Tidy3d default. + monitor_alpha : float = None + Opacity of the monitors. If ``None``, uses Tidy3d default. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + + ax = self.scene.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) + ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) + ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + return ax + + @equal_aspect + @add_ax_if_none + def plot_sources( + self, + x: float = None, + y: float = None, + z: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + alpha: float = None, + ax: Ax = None, + ) -> Ax: + """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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + alpha : float = None + Opacity of the sources, If ``None`` uses Tidy3d default. + 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. + """ + bounds = self.bounds + for source in self.sources: + ax = source.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + return ax + + @equal_aspect + @add_ax_if_none + def plot_monitors( + self, + x: float = None, + y: float = None, + z: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + alpha: float = None, + ax: Ax = None, + ) -> Ax: + """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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + alpha : float = None + Opacity of the sources, If ``None`` uses Tidy3d default. + 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. + """ + bounds = self.bounds + for monitor in self.monitors: + ax = monitor.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + return ax + + @equal_aspect + @add_ax_if_none + def plot_symmetries( + self, + x: float = None, + y: float = None, + z: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ax: Ax = None, + ) -> Ax: + """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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy 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. + """ + + normal_axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) + + for sym_axis, sym_value in enumerate(self.symmetry): + if sym_value == 0 or sym_axis == normal_axis: + continue + sym_box = self._make_symmetry_box(sym_axis=sym_axis) + plot_params = self._make_symmetry_plot_params(sym_value=sym_value) + ax = sym_box.plot(x=x, y=y, z=z, ax=ax, **plot_params.to_kwargs()) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + return ax + + def _make_symmetry_plot_params(self, sym_value: Symmetry) -> PlotParams: + """Make PlotParams for symmetry.""" + + plot_params = plot_params_symmetry.copy() + + if sym_value == 1: + plot_params = plot_params.copy( + update={"facecolor": "lightsteelblue", "edgecolor": "lightsteelblue", "hatch": "++"} + ) + elif sym_value == -1: + plot_params = plot_params.copy( + update={"facecolor": "goldenrod", "edgecolor": "goldenrod", "hatch": "--"} + ) + + return plot_params + + def _make_symmetry_box(self, sym_axis: Axis) -> Box: + """Construct a :class:`.Box` representing the symmetry to be plotted.""" + rmin, rmax = (list(bound) for bound in self.simulation_bounds) + rmax[sym_axis] = (rmin[sym_axis] + rmax[sym_axis]) / 2 + + return Box.from_bounds(rmin, rmax) + + @abstractmethod + @equal_aspect + @add_ax_if_none + def plot_boundaries( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + **kwargs, + ) -> Ax: + """Plot the simulation boundary conditions 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. + **kwargs + Optional keyword arguments passed to the matplotlib ``LineCollection``. + For details on accepted values, refer to + `Matplotlib's documentation `_. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + @equal_aspect + @add_ax_if_none + def plot_structures( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim_new, vlim_new = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + + return self.scene.plot_structures(x=x, y=y, z=z, ax=ax, hlim=hlim_new, vlim=vlim_new) + + @equal_aspect + @add_ax_if_none + def plot_structures_eps( + self, + x: float = None, + y: float = None, + z: float = None, + freq: float = None, + alpha: float = None, + cbar: bool = True, + reverse: bool = False, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """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. + reverse : bool = False + If ``False``, the highest permittivity is plotted in black. + If ``True``, it is plotteed in white (suitable for black backgrounds). + cbar : bool = True + Whether to plot a colorbar for the relative permittivity. + alpha : float = None + Opacity of the structures being plotted. + Defaults to the structure default alpha. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + + return self.scene.plot_structures_eps( + freq=freq, + cbar=cbar, + alpha=alpha, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + reverse=reverse, + ) + + @equal_aspect + @add_ax_if_none + def plot_structures_heat_conductivity( + self, + x: float = None, + y: float = None, + z: float = None, + alpha: float = None, + cbar: bool = True, + reverse: bool = False, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """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. + reverse : bool = False + If ``False``, the highest permittivity is plotted in black. + If ``True``, it is plotteed in white (suitable for black backgrounds). + cbar : bool = True + Whether to plot a colorbar for the relative permittivity. + alpha : float = None + Opacity of the structures being plotted. + Defaults to the structure default alpha. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + + return self.scene.plot_structures_heat_conductivity( + cbar=cbar, + alpha=alpha, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + reverse=reverse, + ) + + @classmethod + def from_scene(cls, scene: Scene, **kwargs) -> AbstractSimulation: + """Create a simulation from a :class:.`Scene` instance. Must provide additional parameters + to define a valid simulation (for example, ``size``, ``run_time``, ``grid_spec``, etc). + + Parameters + ---------- + scene : :class:.`Scene` + Scene containing structures information. + **kwargs + Other arguments + """ + + return cls( + structures=scene.structures, + medium=scene.medium, + **kwargs, + ) diff --git a/tidy3d/components/base_sim/source.py b/tidy3d/components/base_sim/source.py new file mode 100644 index 000000000..d2089523b --- /dev/null +++ b/tidy3d/components/base_sim/source.py @@ -0,0 +1,21 @@ +"""Abstract base for classes that define simulation sources.""" +from __future__ import annotations +from abc import ABC, abstractmethod +import pydantic.v1 as pydantic + +from ..base import Tidy3dBaseModel + +from ..validators import validate_name_str +from ..viz import PlotParams + + +class AbstractSource(Tidy3dBaseModel, ABC): + """Abstract base class for all sources.""" + + name: str = pydantic.Field(None, title="Name", description="Optional name for the source.") + + @abstractmethod + def plot_params(self) -> PlotParams: + """Default parameters for plotting a Source object.""" + + _name_validator = validate_name_str() diff --git a/tidy3d/components/bc_placement.py b/tidy3d/components/bc_placement.py new file mode 100644 index 000000000..5b48a77c1 --- /dev/null +++ b/tidy3d/components/bc_placement.py @@ -0,0 +1,117 @@ +"""Defines placements for boundary conditions.""" +from __future__ import annotations + +from abc import ABC +from typing import Union, Tuple + +import pydantic.v1 as pd + +from .types import BoxSurface +from .base import Tidy3dBaseModel +from ..exceptions import SetupError + + +class AbstractBCPlacement(ABC, Tidy3dBaseModel): + """Abstract placement for boundary conditions.""" + + +class StructureBoundary(AbstractBCPlacement): + """Placement of boundary conditions on the structure's boundary. + + Example + ------- + >>> bc_placement = StructureBoundary(structure="box") + """ + + structure: str = pd.Field( + title="Structure Name", + description="Name of the structure.", + ) + + +class StructureStructureInterface(AbstractBCPlacement): + """Placement of boundary conditions between two structures. + + Example + ------- + >>> bc_placement = StructureStructureInterface(structures=["box", "sphere"]) + """ + + structures: Tuple[str, str] = pd.Field( + title="Structures", + description="Names of two structures.", + ) + + @pd.validator("structures", always=True) + def unique_names(cls, val): + """Error if the same structure is provided twice""" + if val[0] == val[1]: + raise SetupError( + "The same structure is provided twice in 'StructureStructureInterface'." + ) + return val + + +class MediumMediumInterface(AbstractBCPlacement): + """Placement of boundary conditions between two mediums. + + Example + ------- + >>> bc_placement = MediumMediumInterface(mediums=["dieletric", "metal"]) + """ + + mediums: Tuple[str, str] = pd.Field( + title="Mediums", + description="Names of two mediums.", + ) + + @pd.validator("mediums", always=True) + def unique_names(cls, val): + """Error if the same structure is provided twice""" + if val[0] == val[1]: + raise SetupError("The same medium is provided twice in 'MediumMediumInterface'.") + return val + + +class SimulationBoundary(AbstractBCPlacement): + """Placement of boundary conditions on the simulation box boundary. + + Example + ------- + >>> bc_placement = SimulationBoundary(surfaces=["x-", "x+"]) + """ + + surfaces: Tuple[BoxSurface, ...] = pd.Field( + ("x-", "x+", "y-", "y+", "z-", "z+"), + title="Surfaces", + description="Surfaces of simulation domain where to apply boundary conditions.", + ) + + +class StructureSimulationBoundary(AbstractBCPlacement): + """Placement of boundary conditions on the simulation box boundary covered by the structure. + + Example + ------- + >>> bc_placement = StructureSimulationBoundary(structure="box", surfaces=["y-", "y+"]) + """ + + structure: str = pd.Field( + title="Structure Name", + description="Name of the structure.", + ) + + surfaces: Tuple[BoxSurface, ...] = pd.Field( + ("x-", "x+", "y-", "y+", "z-", "z+"), + title="Surfaces", + description="Surfaces of simulation domain where to apply boundary conditions.", + ) + + +BCPlacementType = Union[ + StructureBoundary, + StructureStructureInterface, + MediumMediumInterface, + SimulationBoundary, + StructureSimulationBoundary, +] diff --git a/tidy3d/components/data/data_array.py b/tidy3d/components/data/data_array.py index dcee75e54..9a8a04cd2 100644 --- a/tidy3d/components/data/data_array.py +++ b/tidy3d/components/data/data_array.py @@ -9,7 +9,7 @@ from ...constants import HERTZ, SECOND, MICROMETER, RADIAN from ...exceptions import DataError, FileError -from ..types import Bound +from ..types import Bound, Axis # maps the dimension names to their attributes DIM_ATTRS = { @@ -115,6 +115,10 @@ def _json_encoder(cls, val): def __eq__(self, other) -> bool: """Whether two data array objects are equal.""" + + if not isinstance(other, xr.DataArray): + return False + if not self.data.shape == other.data.shape or not np.all(self.data == other.data): return False for key, val in self.coords.items(): @@ -296,7 +300,9 @@ def sel_inside(self, bounds: Bound) -> SpatialDataArray: return self.isel(x=inds_list[0], y=inds_list[1], z=inds_list[2]) def does_cover(self, bounds: Bound) -> bool: - """Check whether data fully covers specified by ``bounds`` spatial region. + """Check whether data fully covers specified by ``bounds`` spatial region. If data contains + only one point along a given direction, then it is assumed the data is constant along that + direction and coverage is not checked. Parameters @@ -311,10 +317,61 @@ def does_cover(self, bounds: Bound) -> bool: """ return all( - coord[0] <= smin and coord[-1] >= smax + (coord[0] <= smin and coord[-1] >= smax) or len(coord) == 1 for coord, smin, smax in zip(self.coords.values(), bounds[0], bounds[1]) ) + def reflect(self, axis: Axis, center: float) -> SpatialDataArray: + """Reflect data across the plane define by parameters ``axis`` and ``center`` from right to + left. + + Parameters + ---------- + axis : Literal[0, 1, 2] + Normal direction of the reflection plane. + center : float + Location of the reflection plane along its normal direction. + + Returns + ------- + SpatialDataArray + Data after reflection is performed. + """ + + coords = list(self.coords.values()) + data = np.array(self.data) + + if np.isclose(center, coords[axis].data[0]): + num_duplicates = 1 + elif center > coords[axis].data[0]: + raise DataError("Reflection center must be outside and on the left of the data region.") + else: + num_duplicates = 0 + + shape = np.array(np.shape(data)) + old_len = shape[axis] + shape[axis] = 2 * old_len - num_duplicates + + ind_left = [slice(shape[0]), slice(shape[1]), slice(shape[2])] + ind_right = [slice(shape[0]), slice(shape[1]), slice(shape[2])] + + ind_left[axis] = slice(old_len - 1, None, -1) + ind_right[axis] = slice(old_len - num_duplicates, None) + + new_data = np.zeros(shape) + + new_data[ind_left[0], ind_left[1], ind_left[2]] = data + new_data[ind_right[0], ind_right[1], ind_right[2]] = data + + new_coords = np.zeros(shape[axis]) + new_coords[old_len - num_duplicates :] = coords[axis] + new_coords[old_len - 1 :: -1] = 2 * center - coords[axis] + + coords[axis] = new_coords + coords_dict = dict(zip("xyz", coords)) + + return SpatialDataArray(new_data, coords=coords_dict) + class ScalarFieldDataArray(DataArray): """Spatial distribution in the frequency-domain. @@ -545,6 +602,49 @@ class ChargeDataArray(DataArray): _dims = ("n", "p") +class PointDataArray(DataArray): + """Indexed data array. + + Example + ------- + >>> point_array = PointDataArray( + ... (1+1j) * np.random.random((5, 3)), coords=dict(index=np.arange(5), axis=np.arange(3)), + ... ) + """ + + __slots__ = () + _dims = ("index", "axis") + + +class CellDataArray(DataArray): + """Cell connection data array. + + Example + ------- + >>> cell_array = CellDataArray( + ... (1+1j) * np.random.random((4, 3)), + ... coords=dict(cell_index=np.arange(4), vertex_index=np.arange(3)), + ... ) + """ + + __slots__ = () + _dims = ("cell_index", "vertex_index") + + +class IndexedDataArray(DataArray): + """Indexed data array. + + Example + ------- + >>> indexed_array = IndexedDataArray( + ... (1+1j) * np.random.random((3,)), coords=dict(index=np.arange(3)) + ... ) + """ + + __slots__ = () + _dims = ("index",) + + DATA_ARRAY_TYPES = [ SpatialDataArray, ScalarFieldDataArray, @@ -565,5 +665,8 @@ class ChargeDataArray(DataArray): TriangleMeshDataArray, HeatDataArray, ChargeDataArray, + PointDataArray, + CellDataArray, + IndexedDataArray, ] DATA_ARRAY_MAP = {data_array.__name__: data_array for data_array in DATA_ARRAY_TYPES} diff --git a/tidy3d/components/data/dataset.py b/tidy3d/components/data/dataset.py index b5f4fa418..1cd13767a 100644 --- a/tidy3d/components/data/dataset.py +++ b/tidy3d/components/data/dataset.py @@ -7,16 +7,22 @@ import xarray as xr import numpy as np import pydantic.v1 as pd +from matplotlib.tri import Triangulation +from matplotlib import pyplot as plt +import numbers from .data_array import DataArray from .data_array import ScalarFieldDataArray, ScalarFieldTimeDataArray, ScalarModeFieldDataArray from .data_array import ModeIndexDataArray from .data_array import TriangleMeshDataArray from .data_array import TimeDataArray +from .data_array import PointDataArray, IndexedDataArray, CellDataArray, SpatialDataArray -from ..base import Tidy3dBaseModel -from ..types import Axis -from ...exceptions import DataError +from ..viz import equal_aspect, add_ax_if_none, plot_params_grid +from ..base import Tidy3dBaseModel, cached_property +from ..types import Axis, Bound, ArrayLike, Ax, Coordinate, Literal +from ..types import vtk, requires_vtk +from ...exceptions import DataError, ValidationError, Tidy3dNotImplementedError from ...log import log @@ -32,6 +38,17 @@ class AbstractFieldDataset(Dataset, ABC): def field_components(self) -> Dict[str, DataArray]: """Maps the field components to thier associated data.""" + def apply_phase(self, phase: float) -> AbstractFieldDataset: + """Create a copy where all elements are phase-shifted by a value (in radians).""" + if phase == 0.0: + return self + phasor = np.exp(1j * phase) + field_components_shifted = {} + for fld_name, fld_cmp in self.field_components.items(): + fld_cmp_shifted = phasor * fld_cmp + field_components_shifted[fld_name] = fld_cmp_shifted + return self.updated_copy(**field_components_shifted) + @property @abstractmethod def grid_locations(self) -> Dict[str, str]: @@ -265,6 +282,14 @@ class FieldTimeDataset(ElectromagneticFieldDataset): description="Spatial distribution of the z-component of the magnetic field.", ) + def apply_phase(self, phase: float) -> AbstractFieldDataset: + """Create a copy where all elements are phase-shifted by a value (in radians).""" + + if phase != 0.0: + raise ValueError("Can't apply phase to time-domain field data, which is real-valued.") + + return self + class ModeSolverDataset(ElectromagneticFieldDataset): """Dataset storing scalar components of E and H fields as a function of freq. and mode_index. @@ -329,8 +354,9 @@ class ModeSolverDataset(ElectromagneticFieldDataset): description="Complex-valued effective propagation constants associated with the mode.", ) - n_group: ModeIndexDataArray = pd.Field( + n_group_raw: ModeIndexDataArray = pd.Field( None, + alias="n_group", title="Group Index", description="Index associated with group velocity of the mode.", ) @@ -351,6 +377,17 @@ def k_eff(self): """Imaginary part of the propagation index.""" return self.n_complex.imag + @property + def n_group(self): + """Group index.""" + if self.n_group_raw is None: + log.warning( + "The group index was not computed. To calculate group index, pass " + "'group_index_step = True' in the 'ModeSpec'.", + log_once=True, + ) + return self.n_group_raw + def plot_field(self, *args, **kwargs): """Warn user to use the :class:`.ModeSolver` ``plot_field`` function now.""" raise DeprecationWarning( @@ -423,3 +460,1178 @@ class TimeDataset(Dataset): values: TimeDataArray = pd.Field( ..., title="Values", description="Values as a function of time." ) + + +class UnstructuredGridDataset(Dataset, np.lib.mixins.NDArrayOperatorsMixin, ABC): + """Abstract base for datasets that store unstructured grid data.""" + + points: PointDataArray = pd.Field( + ..., + title="Grid Points", + description="Coordinates of points composing the unstructured grid.", + ) + + values: IndexedDataArray = pd.Field( + ..., + title="Point Values", + description="Values stored at the grid points.", + ) + + cells: CellDataArray = pd.Field( + ..., + title="Grid Cells", + description="Cells composing the unstructured grid specified as connections between grid " + "points.", + ) + + @property + def name(self) -> str: + """Dataset name.""" + # we redirect name to values.name + return self.values.name + + @pd.validator("cells", always=True) + def match_cells_to_vtk_type(cls, val): + """Check that cell connections does not have duplicate points.""" + if vtk is None: + return val + + # using val.astype(np.int32/64) directly causes issues when dataarray are later checked == + return CellDataArray(val.data.astype(vtk["id_type"], copy=False), coords=val.coords) + + @pd.validator("values", always=True) + def number_of_values_matches_points(cls, val, values): + """Check that the number of data values matches the number of grid points.""" + num_values = len(val) + + points = values.get("points") + if points is None: + raise ValidationError("Cannot validate '.values' because '.points' failed validation.") + num_points = len(points) + + if num_points != num_values: + raise ValidationError( + f"The number of data values ({num_values}) does not match the number of grid " + f"points ({num_points})." + ) + return val + + @pd.validator("points", always=True) + def points_right_dims(cls, val): + """Check that point coordinates have the right dimensionality.""" + axis_coords_expected = np.arange(cls._point_dims()) + axis_coords_given = val.axis.data + if np.any(axis_coords_given != axis_coords_expected): + raise ValidationError( + f"Points array is expected to have {axis_coords_expected} coord values along 'axis'" + f" (given: {axis_coords_given})." + ) + return val + + @pd.validator("cells", always=True) + def cells_right_type(cls, val): + """Check that cell are of the right type.""" + vertex_coords_expected = np.arange(cls._cell_num_vertices()) + vertex_coords_given = val.vertex_index.data + if np.any(vertex_coords_given != vertex_coords_expected): + raise ValidationError( + f"Cell connections array is expected to have {vertex_coords_expected} coord values" + f" along 'vertex_index' (given: {vertex_coords_given})." + ) + return val + + @pd.validator("cells", always=True) + def check_cell_vertex_range(cls, val, values): + """Check that cell connections use only defined points.""" + all_point_indices_used = val.data.ravel() + min_index_used = np.min(all_point_indices_used) + max_index_used = np.max(all_point_indices_used) + + points = values.get("points") + if points is None: + raise ValidationError("Cannot validate '.values' because '.points' failed validation.") + num_points = len(points) + + if max_index_used != num_points - 1 or min_index_used != 0: + raise ValidationError( + "Cell connections array uses undefined point indicies in the range " + f"[{min_index_used}, {max_index_used}]. The valid range of point indicies is " + f"[0, {num_points-1}]." + ) + return val + + @pd.validator("cells", always=True) + def check_valid_cells(cls, val): + """Check that cell connections does not have duplicate points.""" + indices = val.data + for i in range(cls._cell_num_vertices() - 1): + for j in range(i + 1, cls._cell_num_vertices()): + if np.any(indices[:, i] == indices[:, j]): + log.warning("Unstructured grid contains degenerate cells.") + return val + + def rename(self, name: str) -> UnstructuredGridDataset: + """Return a renamed array.""" + return self.updated_copy(values=self.values.rename(name)) + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + """Override of numpy functions.""" + + out = kwargs.get("out", ()) + for x in inputs + out: + # Only support operations with the same class or a scalar + if not isinstance(x, (numbers.Number, type(self))): + return Tidy3dNotImplementedError + + # Defer to the implementation of the ufunc on unwrapped values. + inputs = tuple(x.values if isinstance(x, UnstructuredGridDataset) else x for x in inputs) + if out: + kwargs["out"] = tuple( + x.values if isinstance(x, UnstructuredGridDataset) else x for x in out + ) + result = getattr(ufunc, method)(*inputs, **kwargs) + + if type(result) is tuple: + # multiple return values + return tuple(self.updated_copy(values=x) for x in result) + elif method == "at": + # no return value + return None + else: + # one return value + return self.updated_copy(values=result) + + @property + def real(self) -> UnstructuredGridDataset: + """Real part of dataset.""" + return self.updated_copy(values=self.values.real) + + @property + def imag(self) -> UnstructuredGridDataset: + """Imaginary part of dataset.""" + return self.updated_copy(values=self.values.imag) + + @property + def abs(self) -> UnstructuredGridDataset: + """Absolute value of dataset.""" + return self.updated_copy(values=self.values.abs) + + @cached_property + def bounds(self) -> Bound: + """Grid bounds.""" + return tuple(np.min(self.points.data, axis=0)), tuple(np.max(self.points.data, axis=0)) + + @classmethod + @abstractmethod + def _point_dims(cls) -> pd.PositiveInt: + """Dimensionality of stored grid point coordinates.""" + + @cached_property + @abstractmethod + def _points_3d_array(self) -> Bound: + """3D coordinates of grid points.""" + + @classmethod + @abstractmethod + def _cell_num_vertices(cls) -> pd.PositiveInt: + """Number of vertices in a cell.""" + + @classmethod + @abstractmethod + @requires_vtk + def _vtk_cell_type(cls): + """VTK cell type to use in the VTK representation.""" + + @cached_property + def _vtk_offsets(self) -> ArrayLike: + """Offsets array to use in the VTK representation.""" + offsets = np.arange(len(self.cells) + 1) * self._cell_num_vertices() + if vtk is None: + return offsets + + return offsets.astype(vtk["id_type"], copy=False) + + @property + @requires_vtk + def _vtk_cells(self): + """VTK cell array to use in the VTK representation.""" + cells = vtk["mod"].vtkCellArray() + cells.SetData( + vtk["numpy_to_vtkIdTypeArray"](self._vtk_offsets), + vtk["numpy_to_vtkIdTypeArray"](self.cells.data.ravel()), + ) + return cells + + @property + @requires_vtk + def _vtk_points(self): + """VTK point array to use in the VTK representation.""" + pts = vtk["mod"].vtkPoints() + pts.SetData(vtk["numpy_to_vtk"](self._points_3d_array)) + return pts + + @property + @requires_vtk + def _vtk_obj(self): + """A VTK representation (vtkUnstructuredGrid) of the grid.""" + + grid = vtk["mod"].vtkUnstructuredGrid() + + grid.SetPoints(self._vtk_points) + grid.SetCells(self._vtk_cell_type(), self._vtk_cells) + point_data_vtk = vtk["numpy_to_vtk"](self.values.data) + point_data_vtk.SetName(self.values.name) + grid.GetPointData().AddArray(point_data_vtk) + + return grid + + @requires_vtk + def _plane_slice_raw(self, axis: Axis, pos: float): + """Slice data with a plane and return the resulting VTK object.""" + + if pos > self.bounds[1][axis] or pos < self.bounds[0][axis]: + raise DataError( + f"Slicing plane (axis: {axis}, pos: {pos}) does not intersect the unstructured grid " + f"(extent along axis {axis}: {self.bounds[0][axis]}, {self.bounds[1][axis]})." + ) + + origin = [0, 0, 0] + origin[axis] = pos + + normal = [0, 0, 0] + normal[axis] = 1 + + # create cutting plane + plane = vtk["mod"].vtkPlane() + plane.SetOrigin(origin[0], origin[1], origin[2]) + plane.SetNormal(normal[0], normal[1], normal[2]) + + # create cutter + cutter = vtk["mod"].vtkPlaneCutter() + cutter.SetPlane(plane) + cutter.SetInputData(self._vtk_obj) + cutter.InterpolateAttributesOn() + cutter.Update() + + # clean up the slice + cleaner = vtk["mod"].vtkCleanPolyData() + cleaner.SetInputData(cutter.GetOutput()) + cleaner.Update() + + return cleaner.GetOutput() + + @abstractmethod + @requires_vtk + def plane_slice( + self, axis: Axis, pos: float + ) -> Union[SpatialDataArray, UnstructuredGridDataset]: + """Slice data with a plane and return the Tidy3D representation of the result + (``UnstructuredGridDataset``). + + Parameters + ---------- + axis : Axis + The normal direction of the slicing plane. + pos : float + Position of the slicing plane along its normal direction. + + Returns + ------- + Union[SpatialDataArray, UnstructuredGridDataset] + The resulting slice. + """ + + @staticmethod + @requires_vtk + def _read_vtkUnstructuredGrid(fname: str): + """Load a :class:`vtkUnstructuredGrid` from a file.""" + reader = vtk["mod"].vtkXMLUnstructuredGridReader() + reader.SetFileName(fname) + reader.Update() + grid = reader.GetOutput() + + return grid + + @classmethod + @abstractmethod + @requires_vtk + def _from_vtk_obj(cls, vtk_obj) -> UnstructuredGridDataset: + """Initialize from a vtk object.""" + + @classmethod + @requires_vtk + def from_vtu(cls, file: str) -> UnstructuredGridDataset: + """Load unstructured data from a vtu file. + + Parameters + ---------- + fname : str + Full path to the .vtu file to load the unstructured data from. + + Returns + ------- + UnstructuredGridDataset + Unstructured data. + """ + grid = cls._read_vtkUnstructuredGrid(file) + return cls._from_vtk_obj(grid) + + @requires_vtk + def to_vtu(self, fname: str): + """Exports unstructured grid data into a .vtu file. + + Parameters + ---------- + fname : str + Full path to the .vtu file to save the unstructured data to. + """ + + writer = vtk["mod"].vtkXMLUnstructuredGridWriter() + writer.SetFileName(fname) + writer.SetInputData(self._vtk_obj) + writer.Write() + + @classmethod + @requires_vtk + def _get_values_from_vtk(cls, vtk_obj, num_points: pd.PositiveInt) -> IndexedDataArray: + """Get point data values from a VTK object.""" + + point_data = vtk_obj.GetPointData() + num_point_arrays = point_data.GetNumberOfArrays() + + if num_point_arrays == 0: + log.warning( + "No point data is found in a VTK object. '.values' will be initialized to zeros." + ) + values_numpy = np.zeros(num_points) + values_name = None + + else: + array_vtk = point_data.GetAbstractArray(0) + + # currently we assume there is only one point data array provided in the VTK object + if num_point_arrays > 1: + array_name = array_vtk.GetName() + log.warning( + f"{num_point_arrays} point data arrays are found in a VTK object. " + f"Only the first array (name: {array_name}) will be used to initialize " + "'.values' while the rest will be ignored." + ) + + # currently we assume data is scalar + num_components = array_vtk.GetNumberOfComponents() + if num_components > 1: + raise DataError( + f"Found point data array in a VTK object is expected to have only 1 component. Found {num_components} components." + ) + + # check that number of values matches number of grid points + num_tuples = array_vtk.GetNumberOfTuples() + if num_tuples != num_points: + raise DataError( + f"The length of found point data array ({num_tuples}) does not match the number of grid points ({num_points})." + ) + + values_numpy = vtk["vtk_to_numpy"](array_vtk) + values_name = array_vtk.GetName() + + values = IndexedDataArray( + values_numpy, coords=dict(index=np.arange(len(values_numpy))), name=values_name + ) + + return values + + @requires_vtk + def box_clip(self, bounds: Bound) -> UnstructuredGridDataset: + """Clip the unstructured grid using a box defined by ``bounds``. + + Parameters + ---------- + bounds : Tuple[float, float, float], Tuple[float, float float] + Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. + + Returns + ------- + UnstructuredGridDataset + Clipped grid. + """ + + # make and run a VTK clipper + clipper = vtk["mod"].vtkBoxClipDataSet() + clipper.SetOrientation(0) + clipper.SetBoxClip( + bounds[0][0], bounds[1][0], bounds[0][1], bounds[1][1], bounds[0][2], bounds[1][2] + ) + clipper.SetInputData(self._vtk_obj) + clipper.GenerateClipScalarsOn() + clipper.GenerateClippedOutputOff() + clipper.Update() + clip = clipper.GetOutput() + + # cleann grid from unused points + grid_cleaner = vtk["mod"].vtkRemoveUnusedPoints() + grid_cleaner.SetInputData(clip) + grid_cleaner.GenerateOriginalPointIdsOff() + grid_cleaner.Update() + clean_clip = grid_cleaner.GetOutput() + + # no intersection check + if clean_clip.GetNumberOfPoints() == 0: + raise DataError("Clipping box does not intersect the unstructured grid.") + + return self._from_vtk_obj(clean_clip) + + @requires_vtk + def interp( + self, + x: Union[float, ArrayLike], + y: Union[float, ArrayLike], + z: Union[float, ArrayLike], + fill_value: float = 0, + ) -> SpatialDataArray: + """Interpolate data at provided x, y, and z. + + Parameters + ---------- + x : Union[float, ArrayLike] + x-coordinates of sampling points. + y : Union[float, ArrayLike] + y-coordinates of sampling points. + z : Union[float, ArrayLike] + z-coordinates of sampling points. + fill_value : float = 0 + Value to use when filling points without interpolated values. + + Returns + ------- + SpatialDataArray + Interpolated data. + """ + + # calculate the resulting array shape + x = np.atleast_1d(x) + y = np.atleast_1d(y) + z = np.atleast_1d(z) + shape = (len(x), len(y), len(z)) + + # create a VTK rectilinear grid to sample onto + structured_grid = vtk["mod"].vtkRectilinearGrid() + structured_grid.SetDimensions(shape) + structured_grid.SetXCoordinates(vtk["numpy_to_vtk"](x)) + structured_grid.SetYCoordinates(vtk["numpy_to_vtk"](y)) + structured_grid.SetZCoordinates(vtk["numpy_to_vtk"](z)) + + # create and execute VTK interpolator + interpolator = vtk["mod"].vtkResampleWithDataSet() + interpolator.SetInputData(structured_grid) + interpolator.SetSourceData(self._vtk_obj) + interpolator.Update() + interpolated = interpolator.GetOutput() + + # get results in a numpy representation + array_id = 0 if self.values.name is None else self.values.name + values_numpy = vtk["vtk_to_numpy"](interpolated.GetPointData().GetAbstractArray(array_id)) + + # fill points without interpolated values + if fill_value != 0: + mask = vtk["vtk_to_numpy"]( + interpolated.GetPointData().GetAbstractArray("vtkValidPointMask") + ) + values_numpy[mask != 1] = fill_value + + # VTK arrays are the z-y-x order, reorder interpolation results to x-y-z order + values_reordered = np.transpose(np.reshape(values_numpy, shape[::-1]), (2, 1, 0)) + + return SpatialDataArray(values_reordered, coords=dict(x=x, y=y, z=z), name=self.values.name) + + @abstractmethod + @requires_vtk + def sel( + self, + x: Union[float, ArrayLike] = None, + y: Union[float, ArrayLike] = None, + z: Union[float, ArrayLike] = None, + ) -> Union[UnstructuredGridDataset, SpatialDataArray]: + """Extract/interpolate data along one or more Cartesian directions. At least of x, y, and z + must be provided. + + Parameters + ---------- + x : Union[float, ArrayLike] = None + x-coordinate of the slice. + y : Union[float, ArrayLike] = None + y-coordinate of the slice. + z : Union[float, ArrayLike] = None + z-coordinate of the slice. + + Returns + ------- + Union[TriangularGridDataset, SpatialDataArray] + Extracted data. + """ + + @requires_vtk + def reflect( + self, axis: Axis, center: float, reflection_only: bool = False + ) -> UnstructuredGridDataset: + """Reflect unstructured data across the plane define by parameters ``axis`` and ``center``. + By default the original data is preserved, setting ``reflection_only`` to ``True`` will + produce only deflected data. + + Parameters + ---------- + axis : Literal[0, 1, 2] + Normal direction of the reflection plane. + center : float + Location of the reflection plane along its normal direction. + reflection_only : bool = False + Return only reflected data. + + Returns + ------- + UnstructuredGridDataset + Data after reflextion is performed. + """ + + reflector = vtk["mod"].vtkReflectionFilter() + reflector.SetPlane([reflector.USE_X, reflector.USE_Y, reflector.USE_Z][axis]) + reflector.SetCenter(center) + reflector.SetCopyInput(not reflection_only) + reflector.SetInputData(self._vtk_obj) + reflector.Update() + + return self._from_vtk_obj(reflector.GetOutput()) + + +class TriangularGridDataset(UnstructuredGridDataset): + """Dataset for storing triangular grid data. + + Note + ---- + To use full functionality of unstructured datasets one must install ``vtk`` package (``pip + install tidy3d[vtk]`` or ``pip install vtk``). Otherwise the functionality of unstructured + datasets is limited to creation, writing to/loading from a file, and arithmetic manipulations. + + Example + ------- + >>> tri_grid_points = PointDataArray( + ... [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], + ... coords=dict(index=np.arange(4), axis=np.arange(2)), + ... ) + >>> + >>> tri_grid_cells = CellDataArray( + ... [[0, 1, 2], [1, 2, 3]], + ... coords=dict(cell_index=np.arange(2), vertex_index=np.arange(3)), + ... ) + >>> + >>> tri_grid_values = IndexedDataArray( + ... [1.0, 2.0, 3.0, 4.0], coords=dict(index=np.arange(4)), + ... ) + >>> + >>> tri_grid = TriangularGridDataset( + ... normal_axis=1, + ... normal_pos=0, + ... points=tri_grid_points, + ... cells=tri_grid_cells, + ... values=tri_grid_values, + ... ) + """ + + normal_axis: Axis = pd.Field( + ..., + title="Grid Axis", + description="Orientation of the grid.", + ) + + normal_pos: float = pd.Field( + ..., + title="Position", + description="Coordinate of the grid along the normal direction.", + ) + + @cached_property + def bounds(self) -> Bound: + """Grid bounds.""" + bounds_2d = super().bounds + bounds_3d = self._points_2d_to_3d(bounds_2d) + return tuple(bounds_3d[0]), tuple(bounds_3d[1]) + + @classmethod + def _point_dims(cls) -> pd.PositiveInt: + """Dimensionality of stored grid point coordinates.""" + return 2 + + def _points_2d_to_3d(self, pts: ArrayLike) -> ArrayLike: + """Convert 2d points into 3d points.""" + return np.insert(pts, obj=self.normal_axis, values=self.normal_pos, axis=1) + + @cached_property + def _points_3d_array(self) -> ArrayLike: + """3D representation of grid points.""" + return self._points_2d_to_3d(self.points.data) + + @classmethod + def _cell_num_vertices(cls) -> pd.PositiveInt: + """Number of vertices in a cell.""" + return 3 + + @classmethod + @requires_vtk + def _vtk_cell_type(cls): + """VTK cell type to use in the VTK representation.""" + return vtk["mod"].VTK_TRIANGLE + + @classmethod + @requires_vtk + def _from_vtk_obj(cls, vtk_obj): + """Initialize from a vtkUnstructuredGrid instance.""" + + # get points cells data from vtk object + if isinstance(vtk_obj, vtk["mod"].vtkPolyData): + cells_vtk = vtk_obj.GetPolys() + elif isinstance(vtk_obj, vtk["mod"].vtkUnstructuredGrid): + cells_vtk = vtk_obj.GetCells() + + cells_numpy = vtk["vtk_to_numpy"](cells_vtk.GetConnectivityArray()) + + cell_offsets = vtk["vtk_to_numpy"](cells_vtk.GetOffsetsArray()) + if not np.all(np.diff(cell_offsets) == cls._cell_num_vertices()): + raise DataError( + "Only triangular 'vtkUnstructuredGrid' or 'vtkPolyData' can be converted into " + "'TriangularGridDataset'." + ) + + points_numpy = vtk["vtk_to_numpy"](vtk_obj.GetPoints().GetData()) + + # data values are read directly into Tidy3D array + values = cls._get_values_from_vtk(vtk_obj, len(points_numpy)) + + # detect zero size dimension + bounds = np.max(points_numpy, axis=0) - np.min(points_numpy, axis=0) + zero_dims = np.where(np.isclose(bounds, 0))[0] + + if len(zero_dims) != 1: + raise DataError( + f"Provided vtk grid does not represent a two dimensional grid. Found zero size dimensions are {zero_dims}." + ) + + normal_axis = zero_dims[0] + normal_pos = points_numpy[0][normal_axis] + tan_dims = [0, 1, 2] + tan_dims.remove(normal_axis) + + # convert 3d coordinates into 2d + points_2d_numpy = points_numpy[:, tan_dims] + + # create Tidy3D points and cells arrays + num_cells = len(cells_numpy) // cls._cell_num_vertices() + cells_numpy = np.reshape(cells_numpy, (num_cells, cls._cell_num_vertices())) + + cells = CellDataArray( + cells_numpy, + coords=dict( + cell_index=np.arange(num_cells), vertex_index=np.arange(cls._cell_num_vertices()) + ), + ) + + points = PointDataArray( + points_2d_numpy, + coords=dict(index=np.arange(len(points_numpy)), axis=np.arange(cls._point_dims())), + ) + + return cls( + normal_axis=normal_axis, + normal_pos=normal_pos, + points=points, + cells=cells, + values=values, + ) + + @requires_vtk + def plane_slice(self, axis: Axis, pos: float) -> SpatialDataArray: + """Slice data with a plane and return the resulting line as a SpatialDataArray. + + Parameters + ---------- + axis : Axis + The normal direction of the slicing plane. + pos : float + Position of the slicing plane along its normal direction. + + Returns + ------- + SpatialDataArray + The resulting slice. + """ + + if axis == self.normal_axis: + raise DataError( + f"Triangular grid (normal: {self.normal_axis}) cannot be sliced by a parallel " + "plane." + ) + + # perform slicing in vtk and get unprocessed points and values + slice_vtk = self._plane_slice_raw(axis=axis, pos=pos) + points_numpy = vtk["vtk_to_numpy"](slice_vtk.GetPoints().GetData()) + values = self._get_values_from_vtk(slice_vtk, len(points_numpy)) + + # axis of the resulting line + slice_axis = 3 - self.normal_axis - axis + + # sort found intersection in ascending order + sorting = np.argsort(points_numpy[:, slice_axis], kind="mergesort") + + # assemble coords for SpatialDataArray + coords = [None, None, None] + coords[axis] = [pos] + coords[self.normal_axis] = [self.normal_pos] + coords[slice_axis] = points_numpy[sorting, slice_axis] + coords_dict = dict(zip("xyz", coords)) + + # reshape values from a 1d array into a 3d array + new_shape = [1, 1, 1] + new_shape[slice_axis] = len(values) + values_reshaped = np.reshape(values.data[sorting], new_shape) + + return SpatialDataArray(values_reshaped, coords=coords_dict, name=self.values.name) + + @property + def _triangulation_obj(self) -> Triangulation: + """Matplotlib triangular representation of the grid to use in plotting.""" + return Triangulation(self.points[:, 0], self.points[:, 1], self.cells) + + @equal_aspect + @add_ax_if_none + def plot( + self, + ax: Ax = None, + field: bool = True, + grid: bool = True, + cbar: bool = True, + cmap: str = "viridis", + vmin: float = None, + vmax: float = None, + shading: Literal["gourand", "flat"] = "gouraud", + cbar_kwargs: Dict = None, + ) -> Ax: + """Plot the data field and/or the unstructured grid. + + Parameters + ---------- + ax : matplotlib.axes._subplots.Axes = None + matplotlib axes to plot on, if not specified, one is created. + field : bool = True + Whether to plot the data field. + grid : bool = True + Whether to plot the unstructured grid. + cbar : bool = True + Display colorbar (only if `field == True`). + cmap : str = "viridis" + Color map to use for plotting. + vmin : float = None + The lower bound of data range that the colormap covers. If `None`, they are + inferred from the data and other keyword arguments. + vmax : float = None + The upper bound of data range that the colormap covers. If `None`, they are + inferred from the data and other keyword arguments. + shading : Literal["gourand", "flat"] = "gourand" + Type of shading to use when plotting the data field. + cbar_kwargs : Dict = {} + Additional parameters passed to colorbar object. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + if cbar_kwargs is None: + cbar_kwargs = {} + if not (field or grid): + raise DataError("Nothing to plot ('field == False', 'grid == False').") + + # plot data field if requested + if field: + plot_obj = ax.tripcolor( + self._triangulation_obj, + self.values.data, + shading=shading, + cmap=cmap, + vmin=vmin, + vmax=vmax, + ) + + if cbar: + label_kwargs = {} + if "label" not in cbar_kwargs: + label_kwargs["label"] = self.values.name + plt.colorbar(plot_obj, **cbar_kwargs, **label_kwargs) + + # plot grid if requested + if grid: + ax.triplot( + self._triangulation_obj, + color=plot_params_grid.edgecolor, + linewidth=plot_params_grid.linewidth, + ) + + # set labels and titles + ax_labels = ["x", "y", "z"] + normal_axis_name = ax_labels.pop(self.normal_axis) + ax.set_xlabel(ax_labels[0]) + ax.set_ylabel(ax_labels[1]) + ax.set_title(f"{normal_axis_name} = {self.normal_pos}") + return ax + + @requires_vtk + def interp( + self, + x: Union[float, ArrayLike], + y: Union[float, ArrayLike], + z: Union[float, ArrayLike], + fill_value: float = 0, + ignore_normal_pos: bool = True, + ) -> SpatialDataArray: + """Interpolate data at provided x, y, and z. + + Parameters + ---------- + x : Union[float, ArrayLike] + x-coordinates of sampling points. + y : Union[float, ArrayLike] + y-coordinates of sampling points. + z : Union[float, ArrayLike] + z-coordinates of sampling points. + fill_value : float = 0 + Value to use when filling points without interpolated values. + ignore_normal_pos : bool = True + Assume data is invariant along the normal direction to the grid plane. + + Returns + ------- + SpatialDataArray + Interpolated data. + """ + + x = np.atleast_1d(x) + y = np.atleast_1d(y) + z = np.atleast_1d(z) + + if ignore_normal_pos: + xyz = [x, y, z] + xyz[self.normal_axis] = [self.normal_pos] + interp_inplane = super().interp(**dict(zip("xyz", xyz)), fill_value=fill_value) + interp_broadcasted = np.broadcast_to( + interp_inplane, [len(np.atleast_1d(comp)) for comp in [x, y, z]] + ) + + return SpatialDataArray( + interp_broadcasted, coords=dict(x=x, y=y, z=z), name=self.values.name + ) + + return super().interp(x=x, y=y, z=z, fill_value=fill_value) + + @requires_vtk + def sel( + self, + x: Union[float, ArrayLike] = None, + y: Union[float, ArrayLike] = None, + z: Union[float, ArrayLike] = None, + ) -> SpatialDataArray: + """Extract/interpolate data along one or more Cartesian directions. At least of x, y, and z + must be provided. + + Parameters + ---------- + x : Union[float, ArrayLike] = None + x-coordinate of the slice. + y : Union[float, ArrayLike] = None + y-coordinate of the slice. + z : Union[float, ArrayLike] = None + z-coordinate of the slice. + + Returns + ------- + SpatialDataArray + Extracted data. + """ + + xyz = [x, y, z] + axes = [ind for ind, comp in enumerate(xyz) if comp is not None] + num_provided = len(axes) + + if self.normal_axis in axes: + if xyz[self.normal_axis] != self.normal_pos: + raise DataError( + f"No data for {'xyz'[self.normal_axis]} = {xyz[self.normal_axis]} (unstructured" + f" grid is defined at {'xyz'[self.normal_axis]} = {self.normal_pos})." + ) + + if num_provided < 3: + num_provided -= 1 + axes.remove(self.normal_axis) + + if num_provided == 0: + raise DataError("At least one of 'x', 'y', and 'z' must be specified.") + + if num_provided == 1: + axis = axes[0] + return self.plane_slice(axis=axis, pos=xyz[axis]) + + if num_provided == 2: + pos = [x, y, z] + pos[self.normal_axis] = [self.normal_pos] + return self.interp(x=pos[0], y=pos[1], z=pos[2]) + + if num_provided == 3: + return self.interp(x=x, y=y, z=z) + + @requires_vtk + def reflect( + self, axis: Axis, center: float, reflection_only: bool = False + ) -> UnstructuredGridDataset: + """Reflect unstructured data across the plane define by parameters ``axis`` and ``center``. + By default the original data is preserved, setting ``reflection_only`` to ``True`` will + produce only deflected data. + + Parameters + ---------- + axis : Literal[0, 1, 2] + Normal direction of the reflection plane. + center : float + Location of the reflection plane along its normal direction. + reflection_only : bool = False + Return only reflected data. + + Returns + ------- + UnstructuredGridDataset + Data after reflextion is performed. + """ + + # disallow reflecting along normal direction + if axis == self.normal_axis: + raise DataError("Reflection in the normal direction to the grid is prohibited.") + + return super().reflect(axis=axis, center=center, reflection_only=reflection_only) + + +class TetrahedralGridDataset(UnstructuredGridDataset): + """Dataset for storing tetrahedral grid data. + + Note + ---- + To use full functionality of unstructured datasets one must install ``vtk`` package (``pip + install tidy3d[vtk]`` or ``pip install vtk``). Otherwise the functionality of unstructured + datasets is limited to creation, writing to/loading from a file, and arithmetic manipulations. + + Example + ------- + >>> tet_grid_points = PointDataArray( + ... [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]], + ... coords=dict(index=np.arange(4), axis=np.arange(3)), + ... ) + >>> + >>> tet_grid_cells = CellDataArray( + ... [[0, 1, 2, 3]], + ... coords=dict(cell_index=np.arange(1), vertex_index=np.arange(4)), + ... ) + >>> + >>> tet_grid_values = IndexedDataArray( + ... [1.0, 2.0, 3.0, 4.0], coords=dict(index=np.arange(4)), + ... ) + >>> + >>> tet_grid = TetrahedralGridDataset( + ... points=tet_grid_points, + ... cells=tet_grid_cells, + ... values=tet_grid_values, + ... ) + """ + + @classmethod + def _point_dims(cls) -> pd.PositiveInt: + """Dimensionality of stored grid point coordinates.""" + return 3 + + @cached_property + def _points_3d_array(self) -> Bound: + """3D coordinates of grid points.""" + return self.points.data + + @classmethod + def _cell_num_vertices(cls) -> pd.PositiveInt: + """Number of vertices in a cell.""" + return 4 + + @classmethod + @requires_vtk + def _vtk_cell_type(cls): + """VTK cell type to use in the VTK representation.""" + return vtk["mod"].VTK_TETRA + + @classmethod + @requires_vtk + def _from_vtk_obj(cls, grid) -> TetrahedralGridDataset: + """Initialize from a vtkUnstructuredGrid instance.""" + + # read point, cells, and values info from a vtk instance + cells_numpy = vtk["vtk_to_numpy"](grid.GetCells().GetConnectivityArray()) + points_numpy = vtk["vtk_to_numpy"](grid.GetPoints().GetData()) + values = cls._get_values_from_vtk(grid, len(points_numpy)) + + # verify cell_types + cells_types = vtk["vtk_to_numpy"](grid.GetCellTypesArray()) + if not np.all(cells_types == cls._vtk_cell_type()): + raise DataError("Only tetrahedral 'vtkUnstructuredGrid' is currently supported") + + # pack point and cell information into Tidy3D arrays + num_cells = len(cells_numpy) // cls._cell_num_vertices() + cells_numpy = np.reshape(cells_numpy, (num_cells, cls._cell_num_vertices())) + + cells = CellDataArray( + cells_numpy, + coords=dict( + cell_index=np.arange(num_cells), vertex_index=np.arange(cls._cell_num_vertices()) + ), + ) + + points = PointDataArray( + points_numpy, + coords=dict(index=np.arange(len(points_numpy)), axis=np.arange(cls._point_dims())), + ) + + return cls(points=points, cells=cells, values=values) + + @requires_vtk + def plane_slice(self, axis: Axis, pos: float) -> TriangularGridDataset: + """Slice data with a plane and return the resulting :class:.`TriangularGridDataset`. + + Parameters + ---------- + axis : Axis + The normal direction of the slicing plane. + pos : float + Position of the slicing plane along its normal direction. + + Returns + ------- + TriangularGridDataset + The resulting slice. + """ + + slice_vtk = self._plane_slice_raw(axis=axis, pos=pos) + + return TriangularGridDataset._from_vtk_obj(slice_vtk) + + @requires_vtk + def line_slice(self, axis: Axis, pos: Coordinate) -> SpatialDataArray: + """Slice data with a line and return the resulting :class:.`SpatialDataArray`. + + Parameters + ---------- + axis : Axis + The axis of the slicing line. + pos : Tuple[float, float, float] + Position of the slicing line. + + Returns + ------- + SpatialDataArray + The resulting slice. + """ + + bounds = self.bounds + start = list(pos) + end = list(pos) + + start[axis] = bounds[0][axis] + end[axis] = bounds[1][axis] + + # create cutting plane + line = vtk["mod"].vtkLineSource() + line.SetPoint1(start) + line.SetPoint2(end) + line.SetResolution(1) + + # this should be done using vtkProbeLineFilter + # but for some reason it crashes Python + # so, we use a workaround: + # 1) extract cells that are intersected by line (to speed up further slicing) + # 2) do plane slice along first direction + # 3) do second plane slice along second direction + + prober = vtk["mod"].vtkExtractCellsAlongPolyLine() + prober.SetSourceConnection(line.GetOutputPort()) + prober.SetInputData(self._vtk_obj) + prober.Update() + + extracted_cells_vtk = prober.GetOutput() + + if extracted_cells_vtk.GetNumberOfPoints() == 0: + raise DataError("Slicing line does not intersect the unstructured grid.") + + extracted_cells = TetrahedralGridDataset._from_vtk_obj(extracted_cells_vtk) + + tan_dims = [0, 1, 2] + tan_dims.remove(axis) + + # first plane slice + plane_slice = extracted_cells.plane_slice(axis=tan_dims[0], pos=pos[tan_dims[0]]) + # second plane slice + line_slice = plane_slice.plane_slice(axis=tan_dims[1], pos=pos[tan_dims[1]]) + + return line_slice + + @requires_vtk + def sel( + self, + x: Union[float, ArrayLike] = None, + y: Union[float, ArrayLike] = None, + z: Union[float, ArrayLike] = None, + ) -> Union[TriangularGridDataset, SpatialDataArray]: + """Extract/interpolate data along one or more Cartesian directions. At least of x, y, and z + must be provided. + + Parameters + ---------- + x : Union[float, ArrayLike] = None + x-coordinate of the slice. + y : Union[float, ArrayLike] = None + y-coordinate of the slice. + z : Union[float, ArrayLike] = None + z-coordinate of the slice. + + Returns + ------- + Union[TriangularGridDataset, SpatialDataArray] + Extracted data. + """ + + xyz = [x, y, z] + axes = [ind for ind, comp in enumerate(xyz) if comp is not None] + + num_provided = len(axes) + + if num_provided < 3 and any(not np.isscalar(comp) for comp in xyz if comp is not None): + raise DataError( + "Providing x, y, or z as array is only allowed for interpolation. That is, when all" + " three x, y, and z are provided or method '.interp()' is used explicitly." + ) + + if num_provided == 0: + raise DataError("At least one of 'x', 'y', and 'z' must be specified.") + + if num_provided == 1: + axis = axes[0] + return self.plane_slice(axis=axis, pos=xyz[axis]) + + if num_provided == 2: + axis = 3 - axes[0] - axes[1] + xyz[axis] = 0 + return self.line_slice(axis=axis, pos=xyz) + + if num_provided == 3: + return self.interp(x=x, y=y, z=z) + + +UnstructuredGridDatasetType = Union[TriangularGridDataset, TetrahedralGridDataset] diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index b30cf4250..9fd1a3292 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -35,11 +35,13 @@ from ...constants import ETA_0, C_0, MICROMETER from ...log import log +from ..base_sim.data.monitor_data import AbstractMonitorData + Coords1D = ArrayFloat1D -class MonitorData(Dataset, ABC): +class MonitorData(AbstractMonitorData, ABC): """Abstract base class of objects that store data pertaining to a single :class:`.monitor`.""" monitor: MonitorType = pd.Field( @@ -54,11 +56,6 @@ def symmetry_expanded(self) -> MonitorData: """Return self with symmetry applied.""" return self - @property - def symmetry_expanded_copy(self) -> MonitorData: - """Return copy of self with symmetry applied.""" - return self.copy() - def normalize(self, source_spectrum_fn: Callable[[float], complex]) -> Dataset: """Return copy of self after normalization is applied using source spectrum function.""" return self.copy() @@ -846,8 +843,8 @@ def poynting(self) -> ScalarFieldTimeDataArray: # Tangential fields are ordered as E1, E2, H1, H2 tan_fields = self._colocated_tangential_fields dim1, dim2 = self._tangential_dims - e_x_h = tan_fields["E" + dim1] * tan_fields["H" + dim2] - e_x_h -= tan_fields["E" + dim2] * tan_fields["H" + dim1] + e_x_h = np.real(tan_fields["E" + dim1]) * np.real(tan_fields["H" + dim2]) + e_x_h -= np.real(tan_fields["E" + dim2]) * np.real(tan_fields["H" + dim1]) return e_x_h @cached_property @@ -1124,7 +1121,8 @@ def _reorder_modes( ] # Apply phase shift - field_sorted.data = field_sorted.data * np.exp(-1j * phase[None, None, None, :, :]) + phase_fact = np.exp(-1j * phase[None, None, None, :, :]).astype(field_sorted.data.dtype) + field_sorted.data = field_sorted.data * phase_fact update_dict[field_name] = field_sorted @@ -1179,7 +1177,7 @@ def _group_index_post_process(self, frequency_step: float) -> ModeSolverData: ) # remove data corresponding to frequencies used only for group index calculation - update_dict = {"n_complex": self.n_complex.isel(f=center), "n_group": n_group} + update_dict = {"n_complex": self.n_complex.isel(f=center), "n_group_raw": n_group} for key, field in self.field_components.items(): update_dict[key] = field.isel(f=center) @@ -1313,7 +1311,7 @@ def modes_info(self) -> xr.Dataset: "wg TE fraction": self.pol_fraction_waveguide["te"], "wg TM fraction": self.pol_fraction_waveguide["tm"], "mode area": self.mode_area, - "group index": self.n_group, + "group index": self.n_group_raw, # Use raw field to avoid issuing a warning } return xr.Dataset(data_vars=info) @@ -1394,8 +1392,9 @@ class ModeData(MonitorData): description="Complex-valued effective propagation constants associated with the mode.", ) - n_group: ModeIndexDataArray = pd.Field( + n_group_raw: ModeIndexDataArray = pd.Field( None, + alias="n_group", title="Group Index", description="Index associated with group velocity of the mode.", ) @@ -1410,6 +1409,17 @@ def k_eff(self): """Imaginary part of the propagation index.""" return self.n_complex.imag + @property + def n_group(self): + """Group index.""" + if self.n_group_raw is None: + log.warning( + "The group index was not computed. To calculate group index, pass " + "'group_index_step = True' in the 'ModeSpec'.", + log_once=True, + ) + return self.n_group_raw + def normalize(self, source_spectrum_fn) -> ModeData: """Return copy of self after normalization is applied using source spectrum function.""" source_freq_amps = source_spectrum_fn(self.amps.f)[None, :, None] diff --git a/tidy3d/components/data/sim_data.py b/tidy3d/components/data/sim_data.py index 98ad94a58..101c840c2 100644 --- a/tidy3d/components/data/sim_data.py +++ b/tidy3d/components/data/sim_data.py @@ -1,26 +1,32 @@ """ Simulation Level Data """ from __future__ import annotations -from typing import Dict, Callable, Tuple +from typing import Callable, Tuple, Union +import pathlib import xarray as xr import pydantic.v1 as pd import numpy as np +import h5py +import json from .monitor_data import MonitorDataTypes, MonitorDataType, AbstractFieldData, FieldTimeData -from ..base import Tidy3dBaseModel from ..simulation import Simulation -from ..boundary import BlochBoundary -from ..source import TFSF from ..types import Ax, Axis, annotate_type, FieldVal, PlotScale, ColormapType from ..viz import equal_aspect, add_ax_if_none -from ...exceptions import DataError, Tidy3dKeyError, ValidationError +from ...exceptions import DataError, Tidy3dKeyError from ...log import log +from ..base import JSON_TAG + +from ..base_sim.data.sim_data import AbstractSimulationData DATA_TYPE_MAP = {data.__fields__["monitor"].type_: data for data in MonitorDataTypes} +# maps monitor type (string) to the class of the corresponding data +DATA_TYPE_NAME_MAP = {val.__fields__["monitor"].type_.__name__: val for val in MonitorDataTypes} + -class SimulationData(Tidy3dBaseModel): +class SimulationData(AbstractSimulationData): """Stores data from a collection of :class:`.Monitor` objects in a :class:`.Simulation`. Example @@ -75,45 +81,12 @@ class SimulationData(Tidy3dBaseModel): "associated with the monitors of the original :class:`.Simulation`.", ) - log: str = pd.Field( - None, - title="Solver Log", - description="A string containing the log information from the simulation run.", - ) - diverged: bool = pd.Field( False, title="Diverged", description="A boolean flag denoting whether the simulation run diverged.", ) - def __getitem__(self, monitor_name: str) -> MonitorDataType: - """Get a :class:`.MonitorData` by name. Apply symmetry if applicable.""" - monitor_data = self.monitor_data[monitor_name] - return monitor_data.symmetry_expanded_copy - - @property - def monitor_data(self) -> Dict[str, MonitorDataType]: - """Dictionary mapping monitor name to its associated :class:`.MonitorData`.""" - return {monitor_data.monitor.name: monitor_data for monitor_data in self.data} - - @pd.validator("data", always=True) - def data_monitors_match_sim(cls, val, values): - """Ensure each MonitorData in ``.data`` corresponds to a monitor in ``.simulation``.""" - sim = values.get("simulation") - if sim is None: - raise ValidationError("Simulation.simulation failed validation, can't validate data.") - for mnt_data in val: - try: - monitor_name = mnt_data.monitor.name - sim.get_monitor_by_name(monitor_name) - except Tidy3dKeyError as exc: - raise DataError( - f"Data with monitor name {monitor_name} supplied " - "but not found in the Simulation" - ) from exc - return val - @property def final_decay_value(self) -> float: """Returns value of the field decay at the final time step.""" @@ -141,16 +114,10 @@ def source_spectrum(self, source_index: int) -> Callable: times = self.simulation.tmesh dt = self.simulation.dt - # get boundary information to determine whether to use complex fields - boundaries = self.simulation.boundary_spec.to_list - boundaries_1d = [boundary_1d for dim_boundary in boundaries for boundary_1d in dim_boundary] - complex_fields = any(isinstance(boundary, BlochBoundary) for boundary in boundaries_1d) - complex_fields = complex_fields and not isinstance(source, TFSF) - # plug in mornitor_data frequency domain information def source_spectrum_fn(freqs): """Source amplitude as function of frequency.""" - spectrum = source_time.spectrum(times, freqs, dt, complex_fields) + spectrum = source_time.spectrum(times, freqs, dt) # Remove user defined amplitude and phase from the normalization # such that they would still have an effect on the output fields. @@ -195,7 +162,7 @@ def load_field_monitor(self, monitor_name: str) -> AbstractFieldData: if not isinstance(mon_data, AbstractFieldData): raise DataError( f"data for monitor '{monitor_name}' does not contain field data " - f"as it is a `{type(mon_data)}`." + f"as it is a '{type(mon_data)}'." ) return mon_data @@ -315,45 +282,9 @@ def get_poynting_vector(self, field_monitor_name: str) -> xr.Dataset: return xr.Dataset(poynting_components) - @staticmethod - def _field_component_value(field_component: xr.DataArray, val: FieldVal) -> xr.DataArray: - """return the desired value of a field component. - - Parameter - ---------- - field_component : xarray.DataArray - Field component from which to calculate the value. - val : Literal['real', 'imag', 'abs', 'abs^2', 'phase'] - Which part of the field to return. - - Returns - ------- - xarray.DataArray - Value extracted from the field component. - """ - if val == "real": - field_value = field_component.real - field_value.name = f"Re{{{field_component.name}}}" - - elif val == "imag": - field_value = field_component.imag - field_value.name = f"Im{{{field_component.name}}}" - - elif val == "abs": - field_value = np.abs(field_component) - field_value.name = f"|{field_component.name}|" - - elif val == "abs^2": - field_value = np.abs(field_component) ** 2 - field_value.name = f"|{field_component.name}|²" - - elif val == "phase": - field_value = np.arctan2(field_component.imag, field_component.real) - field_value.name = f"∠{field_component.name}" - - return field_value - - def _get_scalar_field(self, field_monitor_name: str, field_name: str, val: FieldVal): + def _get_scalar_field( + self, field_monitor_name: str, field_name: str, val: FieldVal, phase: float = 0.0 + ): """return ``xarray.DataArray`` of the scalar field of a given monitor at Yee cell centers. Parameters @@ -364,6 +295,8 @@ def _get_scalar_field(self, field_monitor_name: str, field_name: str, val: Field Name of the derived field component: one of `('E', 'H', 'S', 'Sx', 'Sy', 'Sz')`. val : Literal['real', 'imag', 'abs', 'abs^2', 'phase'] = 'real' Which part of the field to plot. + phase : float = 0.0 + Optional phase to apply to result Returns ------- @@ -383,6 +316,8 @@ def _get_scalar_field(self, field_monitor_name: str, field_name: str, val: Field else: dataset = self.at_boundaries(field_monitor_name) + dataset = self.apply_phase(data=dataset, phase=phase) + if field_name in ("E", "H", "S"): # Gather vector components required_components = [field_name + c for c in "xyz"] @@ -439,6 +374,78 @@ def get_intensity(self, field_monitor_name: str) -> xr.DataArray: field_monitor_name=field_monitor_name, field_name="E", val="abs^2" ) + @classmethod + def mnt_data_from_file(cls, fname: str, mnt_name: str, **parse_obj_kwargs) -> MonitorDataType: + """Loads data for a specific monitor from a .hdf5 file with data for a ``SimulationData``. + + Parameters + ---------- + fname : str + Full path to an hdf5 file containing :class:`.SimulationData` data. + mnt_name : str, optional + `.name` of the monitor to load the data from. + **parse_obj_kwargs + Keyword arguments passed to either pydantic's ``parse_obj`` function when loading model. + + Returns + ------- + :class:`MonitorData` + Monitor data corresponding to the `mnt_name` type. + + Example + ------- + >>> field_data = SimulationData.from_file(fname='folder/data.hdf5', mnt_name="field") # doctest: +SKIP + """ + + if pathlib.Path(fname).suffix != ".hdf5": + raise ValueError("'mnt_data_from_file' only works with '.hdf5' files.") + + # open file and ensure it has data + with h5py.File(fname) as f_handle: + if "data" not in f_handle: + raise ValueError(f"could not find data in the supplied file {fname}") + + # get the monitor list from the json string + json_string = f_handle[JSON_TAG][()] + json_dict = json.loads(json_string) + monitor_list = json_dict["simulation"]["monitors"] + + # loop through data + for monitor_index_str, _mnt_data in f_handle["data"].items(): + + # grab the monitor data for this data element + monitor_dict = monitor_list[int(monitor_index_str)] + + # if a match on the monitor name + if monitor_dict["name"] == mnt_name: + + # try to grab the monitor data type + monitor_type_str = monitor_dict["type"] + if monitor_type_str not in DATA_TYPE_NAME_MAP: + raise ValueError(f"Could not find data type '{monitor_type_str}'.") + monitor_data_type = DATA_TYPE_NAME_MAP[monitor_type_str] + + # load the monitor data from the file using the group_path + group_path = f"data/{monitor_index_str}" + return monitor_data_type.from_file( + fname, group_path=group_path, **parse_obj_kwargs + ) + + raise ValueError(f"No monitor with name '{mnt_name}' found in data file.") + + @staticmethod + def apply_phase(data: Union[xr.DataArray, xr.Dataset], phase: float = 0.0) -> xr.DataArray: + """Apply a phase to xarray data.""" + if phase != 0.0: + if np.any(np.iscomplex(data.values)): + data *= np.exp(1j * phase) + else: + log.warning( + f"Non-zero phase of {phase} specified but the data being plotted is " + "real-valued. The phase will be ignored in the plot." + ) + return data + def plot_field( self, field_monitor_name: str, @@ -446,6 +453,7 @@ def plot_field( val: FieldVal = "real", scale: PlotScale = "lin", eps_alpha: float = 0.2, + phase: float = 0.0, robust: bool = True, vmin: float = None, vmax: float = None, @@ -470,6 +478,9 @@ def plot_field( eps_alpha : float = 0.2 Opacity of the structure permittivity. Must be between 0 and 1 (inclusive). + phase : float = 0.0 + Optional phase (radians) to apply to the fields. + Only has an effect on frequency-domain fields. robust : bool = True If True and vmin or vmax are absent, uses the 2nd and 98th percentiles of the data to compute the color limits. This helps in visualizing the field patterns especially @@ -496,7 +507,6 @@ def plot_field( """ # get the DataArray corresponding to the monitor_name and field_name - # deprecated intensity if field_name == "int": log.warning( @@ -508,7 +518,7 @@ def plot_field( if field_name in ("E", "H") or field_name[0] == "S": # Derived fields - field_data = self._get_scalar_field(field_monitor_name, field_name, val) + field_data = self._get_scalar_field(field_monitor_name, field_name, val, phase=phase) else: # Direct field component (e.g. Ex) field_monitor_data = self.load_field_monitor(field_monitor_name) @@ -516,6 +526,7 @@ def plot_field( raise DataError(f"field_name '{field_name}' not found in data.") field_component = field_monitor_data.field_components[field_name] field_component.name = field_name + field_component = self.apply_phase(data=field_component, phase=phase) field_data = self._field_component_value(field_component, val) if scale == "dB": diff --git a/tidy3d/components/field_projection.py b/tidy3d/components/field_projection.py index 175bf1468..0d7eb0335 100644 --- a/tidy3d/components/field_projection.py +++ b/tidy3d/components/field_projection.py @@ -341,6 +341,7 @@ def _far_fields_for_surface( phi: ArrayLikeN2F, surface: FieldProjectionSurface, currents: xr.Dataset, + medium: MediumType, ): """Compute far fields at an angle in spherical coordinates for a given set of surface currents and observation angles. @@ -358,13 +359,14 @@ def _far_fields_for_surface( :class:`FieldProjectionSurface` object to use as source of near field. currents : xarray.Dataset xarray Dataset containing surface currents associated with the surface monitor. + medium : :class:`.MediumType` + Background medium through which to project fields. Returns ------- tuple(numpy.ndarray[float], ...) ``Er``, ``Etheta``, ``Ephi``, ``Hr``, ``Htheta``, ``Hphi`` for the given surface. """ - pts = [currents[name].values for name in ["x", "y", "z"]] try: @@ -393,7 +395,7 @@ def _far_fields_for_surface( phase = [None] * 3 propagation_factor = -1j * AbstractFieldProjectionData.wavenumber( - medium=self.medium, frequency=frequency + medium=medium, frequency=frequency ) def integrate_for_one_theta(i_th: int): @@ -448,7 +450,7 @@ def integrate_for_one_theta(i_th: int): # Lphi (8.34b) Lphi = -M[0] * sin_phi[None, :] + M[1] * cos_phi[None, :] - eta = ETA_0 / np.sqrt(self.medium.eps_model(frequency)) + eta = ETA_0 / np.sqrt(medium.eps_model(frequency)) Etheta = -(Lphi + eta * Ntheta) Ephi = Ltheta - eta * Nphi @@ -459,6 +461,45 @@ def integrate_for_one_theta(i_th: int): return Er, Etheta, Ephi, Hr, Htheta, Hphi + @staticmethod + def apply_window_to_currents( + proj_monitor: AbstractFieldProjectionMonitor, currents: xr.Dataset + ) -> xr.Dataset: + """Apply windowing function to the surface currents.""" + if proj_monitor.size.count(0.0) == 0: + return currents + if proj_monitor.window_size == (0, 0): + return currents + + pts = [currents[name].values for name in ["x", "y", "z"]] + + custom_bounds = [ + [pts[i][0] for i in range(3)], + [pts[i][-1] for i in range(3)], + ] + + window_size, window_minus, window_plus = proj_monitor.window_parameters( + custom_bounds=custom_bounds + ) + + new_currents = currents.copy(deep=True) + for dim, (dim_name, points) in enumerate(zip("xyz", pts)): + window_fn = proj_monitor.window_function( + points=points, + window_size=window_size, + window_minus=window_minus, + window_plus=window_plus, + dim=dim, + ) + window_data = xr.DataArray( + window_fn, + dims=[dim_name], + coords=[points], + ) + new_currents *= window_data + + return new_currents + def project_fields( self, proj_monitor: AbstractFieldProjectionMonitor ) -> AbstractFieldProjectionData: @@ -507,17 +548,26 @@ def _project_fields_angular( np.zeros((1, len(theta), len(phi), len(freqs)), dtype=complex) for _ in field_names ] - k = AbstractFieldProjectionData.wavenumber(medium=self.medium, frequency=freqs) + medium = monitor.medium if monitor.medium else self.medium + k = AbstractFieldProjectionData.wavenumber(medium=medium, frequency=freqs) phase = np.atleast_1d( AbstractFieldProjectionData.propagation_phase(dist=monitor.proj_distance, k=k) ) for surface in self.surfaces: + # apply windowing to currents + currents = self.apply_window_to_currents(monitor, self.currents[surface.monitor.name]) + if monitor.far_field_approx: for idx_f, frequency in enumerate(freqs): _fields = self._far_fields_for_surface( - frequency, theta, phi, surface, self.currents[surface.monitor.name] + frequency=frequency, + theta=theta, + phi=phi, + surface=surface, + currents=currents, + medium=medium, ) for field, _field in zip(fields, _fields): field[..., idx_f] += _field * phase[idx_f] @@ -534,7 +584,7 @@ def _project_fields_angular( ): _x, _y, _z = monitor.sph_2_car(monitor.proj_distance, _theta, _phi) _fields = self._fields_for_surface_exact( - _x, _y, _z, surface, self.currents[surface.monitor.name] + x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium ) for field, _field in zip(fields, _fields): field[0, i, j, :] += _field @@ -545,7 +595,7 @@ def _project_fields_angular( for name, field in zip(field_names, fields) } return FieldProjectionAngleData( - monitor=monitor, projection_surfaces=self.surfaces, medium=self.medium, **fields + monitor=monitor, projection_surfaces=self.surfaces, medium=medium, **fields ) def _project_fields_cartesian( @@ -576,7 +626,8 @@ def _project_fields_cartesian( np.zeros((len(x), len(y), len(z), len(freqs)), dtype=complex) for _ in field_names ] - wavenumber = AbstractFieldProjectionData.wavenumber(medium=self.medium, frequency=freqs) + medium = monitor.medium if monitor.medium else self.medium + wavenumber = AbstractFieldProjectionData.wavenumber(medium=medium, frequency=freqs) # Zip together all combinations of observation points for better progress tracking iter_coords = [ @@ -596,16 +647,26 @@ def _project_fields_cartesian( for surface in self.surfaces: + # apply windowing to currents + currents = self.apply_window_to_currents( + monitor, self.currents[surface.monitor.name] + ) + if monitor.far_field_approx: for idx_f, frequency in enumerate(freqs): _fields = self._far_fields_for_surface( - frequency, theta, phi, surface, self.currents[surface.monitor.name] + frequency=frequency, + theta=theta, + phi=phi, + surface=surface, + currents=currents, + medium=medium, ) for field, _field in zip(fields, _fields): field[i, j, k, idx_f] += _field * phase[idx_f] else: _fields = self._fields_for_surface_exact( - _x, _y, _z, surface, self.currents[surface.monitor.name] + x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium ) for field, _field in zip(fields, _fields): field[i, j, k, :] += _field @@ -616,7 +677,7 @@ def _project_fields_cartesian( for name, field in zip(field_names, fields) } return FieldProjectionCartesianData( - monitor=monitor, projection_surfaces=self.surfaces, medium=self.medium, **fields + monitor=monitor, projection_surfaces=self.surfaces, medium=medium, **fields ) def _project_fields_kspace( @@ -643,7 +704,8 @@ def _project_fields_kspace( field_names = ("Er", "Etheta", "Ephi", "Hr", "Htheta", "Hphi") fields = [np.zeros((len(ux), len(uy), 1, len(freqs)), dtype=complex) for _ in field_names] - k = AbstractFieldProjectionData.wavenumber(medium=self.medium, frequency=freqs) + medium = monitor.medium if monitor.medium else self.medium + k = AbstractFieldProjectionData.wavenumber(medium=medium, frequency=freqs) phase = np.atleast_1d( AbstractFieldProjectionData.propagation_phase(dist=monitor.proj_distance, k=k) ) @@ -658,10 +720,20 @@ def _project_fields_kspace( for surface in self.surfaces: + # apply windowing to currents + currents = self.apply_window_to_currents( + monitor, self.currents[surface.monitor.name] + ) + if monitor.far_field_approx: for idx_f, frequency in enumerate(freqs): _fields = self._far_fields_for_surface( - frequency, theta, phi, surface, self.currents[surface.monitor.name] + frequency=frequency, + theta=theta, + phi=phi, + surface=surface, + currents=currents, + medium=medium, ) for field, _field in zip(fields, _fields): field[i, j, 0, idx_f] += _field * phase[idx_f] @@ -669,7 +741,7 @@ def _project_fields_kspace( else: _x, _y, _z = monitor.sph_2_car(monitor.proj_distance, theta, phi) _fields = self._fields_for_surface_exact( - _x, _y, _z, surface, self.currents[surface.monitor.name] + x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium ) for field, _field in zip(fields, _fields): field[i, j, 0, :] += _field @@ -685,7 +757,7 @@ def _project_fields_kspace( for name, field in zip(field_names, fields) } return FieldProjectionKSpaceData( - monitor=monitor, projection_surfaces=self.surfaces, medium=self.medium, **fields + monitor=monitor, projection_surfaces=self.surfaces, medium=medium, **fields ) """Exact projections""" @@ -697,6 +769,7 @@ def _fields_for_surface_exact( z: float, surface: FieldProjectionSurface, currents: xr.Dataset, + medium: MediumType, ): """Compute projected fields in spherical coordinates at a given projection point on a Cartesian grid for a given set of surface currents using the exact homogeneous medium @@ -714,6 +787,8 @@ def _fields_for_surface_exact( :class:`FieldProjectionSurface` object to use as source of near field. currents : xarray.Dataset xarray Dataset containing surface currents associated with the surface monitor. + medium : :class:`.MediumType` + Background medium through which to project fields. Returns ------- @@ -721,13 +796,12 @@ def _fields_for_surface_exact( ``Er``, ``Etheta``, ``Ephi``, ``Hr``, ``Htheta``, ``Hphi`` projected fields for each frequency. """ - freqs = np.array(self.frequencies) i_omega = 1j * 2.0 * np.pi * freqs[None, None, None, :] - wavenumber = AbstractFieldProjectionData.wavenumber(frequency=freqs, medium=self.medium) + wavenumber = AbstractFieldProjectionData.wavenumber(frequency=freqs, medium=medium) wavenumber = wavenumber[None, None, None, :] # add space dimensions - eps_complex = self.medium.eps_model(frequency=freqs) + eps_complex = medium.eps_model(frequency=freqs) epsilon = EPSILON_0 * eps_complex[None, None, None, :] # source points diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index cac49924e..5592402c5 100644 --- a/tidy3d/components/geometry/base.py +++ b/tidy3d/components/geometry/base.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import List, Tuple, Any +from typing import List, Tuple, Any, Union, Callable from math import isclose import functools @@ -15,17 +15,63 @@ from ..base import Tidy3dBaseModel, cached_property from ..types import Ax, Axis, PlanePosition, Shapely, ClipOperationType, annotate_type -from ..types import Bound, Size, Coordinate, Coordinate2D, ArrayFloat2D, ArrayFloat3D +from ..types import Bound, Size, Coordinate, Coordinate2D +from ..types import ArrayFloat2D, ArrayFloat3D, MatrixReal4x4, trimesh from ..viz import add_ax_if_none, equal_aspect, PLOT_BUFFER, ARROW_LENGTH from ..viz import PlotParams, plot_params_geometry, polygon_patch, arrow_style from ..transformation import RotationAroundAxis from ...log import log -from ...exceptions import SetupError, ValidationError, Tidy3dKeyError, Tidy3dError +from ...exceptions import SetupError, ValidationError +from ...exceptions import Tidy3dKeyError, Tidy3dError, Tidy3dImportError from ...constants import MICROMETER, LARGE_NUMBER, RADIAN, inf, fp_eps +try: + gdstk_available = True + import gdstk +except ImportError: + gdstk_available = False + +try: + gdspy_available = True + import gdspy +except ImportError: + gdspy_available = False + + POLY_GRID_SIZE = 1e-12 +def requires_trimesh(fn): + """When decorating a method, requires that trimesh is available.""" + + @functools.wraps(fn) + def _fn(*args, **kwargs): + if trimesh is None: + raise Tidy3dImportError( + "The package 'trimesh' is required for this operation, but it was not found. " + "Please install the 'trimesh' dependencies using, for example, " + "'pip install -r requirements/trimesh.txt'." + ) + return fn(*args, **kwargs) + + return _fn + + +_shapely_operations = { + "union": shapely.union, + "intersection": shapely.intersection, + "difference": shapely.difference, + "symmetric_difference": shapely.symmetric_difference, +} + +_bit_operations = { + "union": lambda a, b: a | b, + "intersection": lambda a, b: a & b, + "difference": lambda a, b: a & ~b, + "symmetric_difference": lambda a, b: a != b, +} + + class Geometry(Tidy3dBaseModel, ABC): """Abstract base class, defines where something exists in space.""" @@ -139,10 +185,32 @@ def inside_meshgrid( return is_inside @abstractmethod + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + def intersections_plane( self, x: float = None, y: float = None, z: float = None ) -> List[Shapely]: - """Returns list of shapely geomtries at plane specified by one non-None value of x,y,z. + """Returns list of shapely geometries at plane specified by one non-None value of x,y,z. Parameters ---------- @@ -160,9 +228,17 @@ def intersections_plane( For more details refer to `Shapely's Documentaton `_. """ + axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) + origin = self.unpop_axis(position, (0, 0), axis=axis) + normal = self.unpop_axis(1, (0, 0), axis=axis) + to_2D = np.eye(4) + if axis != 2: + last, indices = self.pop_axis((0, 1, 2), axis) + to_2D = to_2D[list(indices) + [last, 3]] + return self.intersections_tilted_plane(normal, origin, to_2D) def intersections_2dbox(self, plane: Box) -> List[Shapely]: - """Returns list of shapely geomtries representing the intersections of the geometry with + """Returns list of shapely geometries representing the intersections of the geometry with a 2D box. Returns @@ -367,10 +443,22 @@ def plot( def plot_shape(self, shape: Shapely, plot_params: PlotParams, ax: Ax) -> Ax: """Defines how a shape is plotted on a matplotlib axes.""" + if shape.geom_type in ( + "MultiPoint", + "MultiLineString", + "MultiPolygon", + "GeometryCollection", + ): + for sub_shape in shape.geoms: + ax = self.plot_shape(shape=sub_shape, plot_params=plot_params, ax=ax) + + return ax + _shape = Geometry.evaluate_inf_shape(shape) + if _shape.geom_type == "LineString": xs, ys = zip(*_shape.coords) - ax.plot(xs, ys, color=plot_params.facecolor) + ax.plot(xs, ys, color=plot_params.facecolor, linewidth=plot_params.linewidth) elif _shape.geom_type == "Point": ax.scatter(shape.x, shape.y, color=plot_params.facecolor) else: @@ -378,6 +466,25 @@ def plot_shape(self, shape: Shapely, plot_params: PlotParams, ax: Ax) -> Ax: ax.add_artist(patch) return ax + @staticmethod + def _do_not_intersect(bounds_a, bounds_b, shape_a, shape_b): + """Check whether two shapes intersect.""" + + # do a bounding box check to see if any intersection to do anything about + if ( + bounds_a[0] > bounds_b[2] + or bounds_b[0] > bounds_a[2] + or bounds_a[1] > bounds_b[3] + or bounds_b[1] > bounds_a[3] + ): + return True + + # look more closely to see if intersected. + if shape_b.is_empty or not shape_a.intersects(shape_b): + return True + + return False + def _get_plot_labels(self, axis: Axis) -> Tuple[str, str]: """Returns planar coordinate x and y axis labels for cross section plots. @@ -641,6 +748,61 @@ def surface_area(self, bounds: Bound = None): def _surface_area(self, bounds: Bound) -> float: """Returns object's surface area within given bounds.""" + def translated(self, x: float, y: float, z: float) -> Geometry: + """Return a translated copy of this geometry. + + Parameters + ---------- + x : float + Translation along x. + y : float + Translation along y. + z : float + Translation along z. + + Returns + ------- + :class:`Geometry` + Translated copy of this geometry. + """ + return Transformed(geometry=self, transform=Transformed.translation(x, y, z)) + + def scaled(self, x: float = 1.0, y: float = 1.0, z: float = 1.0) -> Geometry: + """Return a scaled copy of this geometry. + + Parameters + ---------- + x : float = 1.0 + Scaling factor along x. + y : float = 1.0 + Scaling factor along y. + z : float = 1.0 + Scaling factor along z. + + Returns + ------- + :class:`Geometry` + Scaled copy of this geometry. + """ + return Transformed(geometry=self, transform=Transformed.scaling(x, y, z)) + + def rotated(self, angle: float, axis: Union[Axis, Coordinate]) -> Geometry: + """Return a rotated copy of this geometry. + + Parameters + ---------- + angle : float + Rotation angle (in radians). + axis : Union[int, Tuple[float, float, float]] + Axis of rotation: 0, 1, or 2 for x, y, and z, respectively, or a 3D vector. + + Returns + ------- + :class:`Geometry` + Rotated copy of this geometry. + """ + return Transformed(geometry=self, transform=Transformed.rotation(angle, axis)) + """ Field and coordinate transformations """ @staticmethod @@ -894,7 +1056,7 @@ def from_gds( dilation: float = 0.0, sidewall_angle: float = 0, reference_plane: PlanePosition = "middle", - ) -> GeometryGroup: + ) -> Geometry: """Import a ``gdstk.Cell`` or a ``gdspy.Cell`` and extrude it into a GeometryGroup. Parameters @@ -926,23 +1088,25 @@ def from_gds( Returns ------- - :class:`GeometryGroup` - Geometry group with geometries created from the 2D data. + :class:`Geometry` + Geometries created from the 2D data. """ - # switch the GDS cell loader function based on the class name string - # TODO: make this more robust in future releases - gds_cell_class_name = str(gds_cell.__class__) - - if "gdstk" in gds_cell_class_name: + if gdstk_available and isinstance(gds_cell, gdstk.Cell): gds_loader_fn = Geometry.load_gds_vertices_gdstk - elif "gdspy" in gds_cell_class_name: + elif gdspy_available and isinstance(gds_cell, gdspy.Cell): gds_loader_fn = Geometry.load_gds_vertices_gdspy + elif "gdstk" in gds_cell.__class__ and not gdstk_available: + raise Tidy3dImportError( + "Module 'gdstk' not found. It is required to import gdstk cells." + ) + elif "gdspy" in gds_cell.__class__ and not gdspy_available: + raise Tidy3dImportError( + "Module 'gdspy' not found. It is required to import to gdspy cells." + ) else: - raise ValueError( - f"Argument 'gds_cell' of type '{gds_cell_class_name}' does not seem to be " - "a 'Cell' instance from 'gdstk' or 'gdspy' modules and, therefore, cannot be " - "loaded by Tidy3D." + raise Tidy3dError( + "Argument 'gds_cell' must be an instance of 'gdstk.Cell' or 'gdspy.Cell'." ) geometries = [] @@ -960,7 +1124,7 @@ def from_gds( consolidated_logger.warning(str(error)) except Tidy3dError as error: consolidated_logger.warning(str(error)) - return GeometryGroup(geometries=geometries) + return geometries[0] if len(geometries) == 1 else GeometryGroup(geometries=geometries) @staticmethod def from_shapely( @@ -1001,6 +1165,201 @@ def from_shapely( """ return from_shapely(shape, axis, slab_bounds, dilation, sidewall_angle, reference_plane) + def to_gdstk( + self, + x: float = None, + y: float = None, + z: float = None, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + ) -> List: + """Convert a Geometry object's planar slice to a .gds type polygon. + + 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. + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + + Return + ------ + List + List of `gdstk.Polygon`. + """ + if not gdstk_available: + raise Tidy3dImportError( + "Python module 'gdstk' not found. Install the module to be able to export shapes " + "using it." + ) + + shapes = self.intersections_plane(x=x, y=y, z=z) + polygons = [] + for shape in shapes: + for vertices in vertices_from_shapely(shape): + if len(vertices) == 1: + polygons.append(gdstk.Polygon(vertices[0], gds_layer, gds_dtype)) + else: + polygons.extend( + gdstk.boolean( + vertices[:1], + vertices[1:], + "not", + layer=gds_layer, + datatype=gds_dtype, + ) + ) + return polygons + + def to_gdspy( + self, + x: float = None, + y: float = None, + z: float = None, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + ) -> List: + """Convert a Geometry object's planar slice to a .gds type polygon. + + 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. + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + + Return + ------ + List + List of `gdspy.Polygon` and `gdspy.PolygonSet`. + """ + if not gdspy_available: + raise Tidy3dImportError( + "Python module 'gdspy' not found. Install the module to be able to export shapes " + "using it." + ) + + shapes = self.intersections_plane(x=x, y=y, z=z) + polygons = [] + for shape in shapes: + for vertices in vertices_from_shapely(shape): + if len(vertices) == 1: + polygons.append(gdspy.Polygon(vertices[0], gds_layer, gds_dtype)) + else: + polygons.append( + gdspy.boolean( + vertices[:1], + vertices[1:], + "not", + layer=gds_layer, + datatype=gds_dtype, + ) + ) + return polygons + + def to_gds( + self, + cell, + x: float = None, + y: float = None, + z: float = None, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + ) -> None: + """Append a Geometry object's planar slice to a .gds cell. + + Parameters + ---------- + cell : ``gdstk.Cell`` or ``gdspy.Cell`` + Cell object to which the generated polygons are added. + 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. + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + """ + if gdstk_available and isinstance(cell, gdstk.Cell): + polygons = self.to_gdstk(x=x, y=y, z=z, gds_layer=gds_layer, gds_dtype=gds_dtype) + if len(polygons) > 0: + cell.add(*polygons) + + elif gdspy_available and isinstance(cell, gdspy.Cell): + polygons = self.to_gdspy(x=x, y=y, z=z, gds_layer=gds_layer, gds_dtype=gds_dtype) + if len(polygons) > 0: + cell.add(polygons) + + elif "gdstk" in cell.__class__ and not gdstk_available: + raise Tidy3dImportError( + "Module 'gdstk' not found. It is required to export shapes to gdstk cells." + ) + elif "gdspy" in cell.__class__ and not gdspy_available: + raise Tidy3dImportError( + "Module 'gdspy' not found. It is required to export shapes to gdspy cells." + ) + else: + raise Tidy3dError( + "Argument 'cell' must be an instance of 'gdstk.Cell' or 'gdspy.Cell'." + ) + + def to_gds_file( + self, + fname: str, + x: float = None, + y: float = None, + z: float = None, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + gds_cell_name: str = "MAIN", + ) -> None: + """Export a Geometry object's planar slice to a .gds file. + + Parameters + ---------- + fname : str + Full path to the .gds file to save the :class:`Geometry` slice to. + 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. + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + gds_cell_name : str = 'MAIN' + Name of the cell created in the .gds file to store the geometry. + """ + if gdstk_available: + library = gdstk.Library() + elif gdspy_available: + library = gdspy.GdsLibrary() + else: + raise Tidy3dImportError( + "Python modules 'gdspy' and 'gdstk' not found. To export geometries to .gds " + "files, please install one of those those modules." + ) + cell = library.new_cell(gds_cell_name) + self.to_gds(cell, x=x, y=y, z=z, gds_layer=gds_layer, gds_dtype=gds_dtype) + library.write_gds(fname) + def _as_union(self) -> List[Geometry]: """Return a list of geometries that, united, make up the given geometry.""" if isinstance(self, GeometryGroup): @@ -1452,6 +1811,55 @@ def surfaces_with_exclusion(cls, size: Size, center: Coordinate, **kwargs): surfaces = [surf for surf in surfaces if surf.name[-2:] not in exclude_surfaces] return surfaces + @requires_trimesh + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + (x0, y0, z0), (x1, y1, z1) = self.bounds + vertices = [ + (x0, y0, z0), # 0 + (x0, y0, z1), # 1 + (x0, y1, z0), # 2 + (x0, y1, z1), # 3 + (x1, y0, z0), # 4 + (x1, y0, z1), # 5 + (x1, y1, z0), # 6 + (x1, y1, z1), # 7 + ] + faces = [ + (0, 1, 3, 2), # -x + (4, 6, 7, 5), # +x + (0, 4, 5, 1), # -y + (2, 3, 7, 6), # +y + (0, 2, 6, 4), # -z + (1, 5, 7, 3), # +z + ] + mesh = trimesh.Trimesh(vertices, faces) + + section = mesh.section(plane_origin=origin, plane_normal=normal) + if section is None: + return [] + path, _ = section.to_planar(to_2D=to_2D) + return path.polygons_full.tolist() + def intersections_plane(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. @@ -1521,7 +1929,7 @@ def inside( return (dist_x <= Lx / 2) * (dist_y <= Ly / 2) * (dist_z <= Lz / 2) def intersections_with(self, other): - """Returns list of shapely geomtries representing the intersections of the geometry with + """Returns list of shapely geometries representing the intersections of the geometry with this 2D box. Returns @@ -1773,6 +2181,245 @@ def _surface_area(self, bounds: Bound) -> float: """Compound subclasses""" +class Transformed(Geometry): + """Class representing a transformed geometry.""" + + geometry: annotate_type(GeometryType) = pydantic.Field( + ..., title="Geometry", description="Base geometry to be transformed." + ) + + transform: MatrixReal4x4 = pydantic.Field( + np.eye(4).tolist(), + title="Transform", + description="Transform matrix applied to the base geometry.", + ) + + @pydantic.validator("transform") + def _transform_is_invertible(cls, val): + # If the transform is not invertible, this will raise an error + _ = np.linalg.inv(val) + return val + + @pydantic.root_validator(skip_on_failure=True) + def _apply_transforms(cls, values): + while isinstance(values["geometry"], Transformed): + inner = values["geometry"] + values["geometry"] = inner.geometry + values["transform"] = np.dot(values["transform"], inner.transform) + return values + + @cached_property + def inverse(self) -> MatrixReal4x4: + """Inverse of this transform.""" + return np.linalg.inv(self.transform) + + @staticmethod + def _vertices_from_bounds(bounds: Bound) -> ArrayFloat2D: + """Return the 8 vertices derived from bounds. + + The vertices are returned as homogeneous coordinates (with 4 components). + + Parameters + ---------- + bounds : Bound + Bounds from which to derive the vertices. + + Returns + ------- + ArrayFloat2D + Array with shape (4, 8) with all vertices from ``bounds``. + """ + (x0, y0, z0), (x1, y1, z1) = bounds + return np.array( + ( + (x0, x0, x0, x0, x1, x1, x1, x1), + (y0, y0, y1, y1, y0, y0, y1, y1), + (z0, z1, z0, z1, z0, z1, z0, z1), + (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), + ) + ) + + @cached_property + def bounds(self) -> Bound: + """Returns bounding box min and max coordinates. + + Returns + ------- + Tuple[float, float, float], Tuple[float, float, float] + Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. + """ + # NOTE (Lucas): The bounds are overestimated because we don't want to calculate + # precise TriangleMesh representations for GeometryGroup or ClipOperation. + vertices = np.dot(self.transform, self._vertices_from_bounds(self.geometry.bounds))[:3] + return (tuple(vertices.min(axis=1)), tuple(vertices.max(axis=1))) + + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + return self.geometry.intersections_tilted_plane( + tuple(np.dot((normal[0], normal[1], normal[2], 0.0), self.transform)[:3]), + tuple(np.dot(self.inverse, (origin[0], origin[1], origin[2], 1.0))[:3]), + np.dot(to_2D, self.transform), + ) + + def inside( + self, x: np.ndarray[float], y: np.ndarray[float], z: np.ndarray[float] + ) -> np.ndarray[bool]: + """For input arrays ``x``, ``y``, ``z`` of arbitrary but identical shape, return an array + with the same shape which is ``True`` for every point in zip(x, y, z) that is inside the + volume of the :class:`Geometry`, and ``False`` otherwise. + + Parameters + ---------- + x : np.ndarray[float] + Array of point positions in x direction. + y : np.ndarray[float] + Array of point positions in y direction. + z : np.ndarray[float] + Array of point positions in z direction. + + Returns + ------- + np.ndarray[bool] + ``True`` for every point that is inside the geometry. + """ + x = np.array(x) + y = np.array(y) + z = np.array(z) + xyz = np.dot(self.inverse, np.vstack((x.flat, y.flat, z.flat, np.ones(x.size)))) + if xyz.shape[1] == 1: + # TODO: This "fix" is required because of a bug in PolySlab.inside (with non-zero sidewall angle) + return self.geometry.inside(xyz[0][0], xyz[1][0], xyz[2][0]).reshape(x.shape) + return self.geometry.inside(xyz[0], xyz[1], xyz[2]).reshape(x.shape) + + def _volume(self, bounds: Bound) -> float: + """Returns object's volume within given bounds.""" + # NOTE (Lucas): Bounds are overestimated. + vertices = np.dot(self.inverse, self._vertices_from_bounds(bounds))[:3] + inverse_bounds = (tuple(vertices.min(axis=1)), tuple(vertices.max(axis=1))) + return abs(np.linalg.det(self.transform)) * self.geometry.volume(inverse_bounds) + + def _surface_area(self, bounds: Bound) -> float: + """Returns object's surface area within given bounds.""" + log.warning("Surface area of transformed elements cannot be calculated.") + return None + + @staticmethod + def translation(x: float, y: float, z: float) -> MatrixReal4x4: + """Return a translation matrix. + + Parameters + ---------- + x : float + Translation along x. + y : float + Translation along y. + z : float + Translation along z. + + Returns + ------- + numpy.ndarray + Transform matrix with shape (4, 4). + """ + return np.array( + [ + (1.0, 0.0, 0.0, x), + (0.0, 1.0, 0.0, y), + (0.0, 0.0, 1.0, z), + (0.0, 0.0, 0.0, 1.0), + ], + dtype=float, + ) + + @staticmethod + def scaling(x: float = 1.0, y: float = 1.0, z: float = 1.0) -> MatrixReal4x4: + """Return a scaling matrix. + + Parameters + ---------- + x : float = 1.0 + Scaling factor along x. + y : float = 1.0 + Scaling factor along y. + z : float = 1.0 + Scaling factor along z. + + Returns + ------- + numpy.ndarray + Transform matrix with shape (4, 4). + """ + if np.isclose((x, y, z), 0.0).any(): + raise Tidy3dError("Scaling factors cannot be zero in any dimensions.") + return np.array( + [ + (x, 0.0, 0.0, 0.0), + (0.0, y, 0.0, 0.0), + (0.0, 0.0, z, 0.0), + (0.0, 0.0, 0.0, 1.0), + ], + dtype=float, + ) + + @staticmethod + def rotation(angle: float, axis: Union[Axis, Coordinate]) -> MatrixReal4x4: + """Return a rotation matrix. + + Parameters + ---------- + angle : float + Rotation angle (in radians). + axis : Union[int, Tuple[float, float, float]] + Axis of rotation: 0, 1, or 2 for x, y, and z, respectively, or a 3D vector. + + Returns + ------- + numpy.ndarray + Transform matrix with shape (4, 4). + """ + transform = np.eye(4) + transform[:3, :3] = RotationAroundAxis(angle=angle, axis=axis).matrix + return transform + + @staticmethod + def preserves_axis(transform: MatrixReal4x4, axis: Axis) -> bool: + """Indicate if the transform preserves the orientation of a given axis. + + Parameters: + transform: MatrixReal4x4 + Transform matrix to check. + axis : int + Axis to check. Values 0, 1, or 2, to check x, y, or z, respectively. + + Returns + ------- + bool + ``True`` if the transformation preserves the axis orientation, ``False`` otherwise. + """ + i = (axis + 1) % 3 + j = (axis + 2) % 3 + return np.isclose(transform[i, axis], 0) and np.isclose(transform[j, axis], 0) + + class ClipOperation(Geometry): """Class representing the result of a set operation between geometries.""" @@ -1818,10 +2465,61 @@ def to_polygon_list(base_geometry: Shapely) -> List[Shapely]: return [base_geometry] return [] + @property + def _shapely_operation(self) -> Callable[[Shapely, Shapely], Shapely]: + """Return a Shapely function equivalent to this operation.""" + result = _shapely_operations.get(self.operation, None) + if not result: + raise ValueError( + "'operation' must be one of 'union', 'intersection', 'difference', or " + "'symmetric_difference'." + ) + return result + + @property + def _bit_operation(self) -> Callable[[Any, Any], Any]: + """Return a function equivalent to this operation using bit operators.""" + result = _bit_operations.get(self.operation, None) + if not result: + raise ValueError( + "'operation' must be one of 'union', 'intersection', 'difference', or " + "'symmetric_difference'." + ) + return result + + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + geom_a = Geometry.evaluate_inf_shape( + shapely.unary_union(self.geometry_a.intersections_tilted_plane(normal, origin, to_2D)) + ) + geom_b = Geometry.evaluate_inf_shape( + shapely.unary_union(self.geometry_b.intersections_tilted_plane(normal, origin, to_2D)) + ) + return ClipOperation.to_polygon_list(self._shapely_operation(geom_a, geom_b)) + def intersections_plane( self, x: float = None, y: float = None, z: float = None ) -> List[Shapely]: - """Returns list of shapely geomtries at plane specified by one non-None value of x,y,z. + """Returns list of shapely geometries at plane specified by one non-None value of x,y,z. Parameters ---------- @@ -1845,20 +2543,7 @@ def intersections_plane( geom_b = Geometry.evaluate_inf_shape( shapely.unary_union(self.geometry_b.intersections_plane(x, y, z)) ) - if self.operation == "union": - result = ClipOperation.to_polygon_list(shapely.union(geom_a, geom_b)) - elif self.operation == "intersection": - result = ClipOperation.to_polygon_list(shapely.intersection(geom_a, geom_b)) - elif self.operation == "difference": - result = ClipOperation.to_polygon_list(shapely.difference(geom_a, geom_b)) - elif self.operation == "symmetric_difference": - result = ClipOperation.to_polygon_list(shapely.symmetric_difference(geom_a, geom_b)) - else: - raise ValueError( - "'operation' must be one of 'union', 'intersection', 'difference', or " - "'symmetric_difference'." - ) - return result + return ClipOperation.to_polygon_list(self._shapely_operation(geom_a, geom_b)) @cached_property def bounds(self) -> Bound: @@ -1911,20 +2596,7 @@ def inside( """ inside_a = self.geometry_a.inside(x, y, z) inside_b = self.geometry_b.inside(x, y, z) - if self.operation == "union": - result = inside_a | inside_b - elif self.operation == "intersection": - result = inside_a & inside_b - elif self.operation == "difference": - result = inside_a & ~inside_b - elif self.operation == "symmetric_difference": - result = inside_a != inside_b - else: - raise ValueError( - "'operation' must be one of 'union', 'intersection', 'difference', or " - "'symmetric_difference'." - ) - return result + return self._bit_operation(inside_a, inside_b) def inside_meshgrid( self, x: np.ndarray[float], y: np.ndarray[float], z: np.ndarray[float] @@ -1948,24 +2620,16 @@ def inside_meshgrid( """ inside_a = self.geometry_a.inside_meshgrid(x, y, z) inside_b = self.geometry_b.inside_meshgrid(x, y, z) - if self.operation == "union": - result = inside_a | inside_b - elif self.operation == "intersection": - result = inside_a & inside_b - elif self.operation == "difference": - result = inside_a & ~inside_b - else: - result = inside_a != inside_b - return result + return self._bit_operation(inside_a, inside_b) def _volume(self, bounds: Bound) -> float: """Returns object's volume within given bounds.""" # Overestimates if self.operation == "intersection": - return min(self.geometry_a.surface_area(bounds), self.geometry_b.surface_area(bounds)) + return min(self.geometry_a.volume(bounds), self.geometry_b.volume(bounds)) if self.operation == "difference": - return self.geometry_a.surface_area(bounds) - return self.geometry_a.surface_area(bounds) + self.geometry_b.surface_area(bounds) + return self.geometry_a.volume(bounds) + return self.geometry_a.volume(bounds) + self.geometry_b.volume(bounds) def _surface_area(self, bounds: Bound) -> float: """Returns object's surface area within given bounds.""" @@ -2007,10 +2671,37 @@ def bounds(self) -> Bound: tuple(max(b[i] for _, b in bounds) for i in range(3)), ) + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + return [ + intersection + for geometry in self.geometries + for intersection in geometry.intersections_tilted_plane(normal, origin, to_2D) + ] + def intersections_plane( self, x: float = None, y: float = None, z: float = None ) -> List[Shapely]: - """Returns list of shapely geomtries at plane specified by one non-None value of x,y,z. + """Returns list of shapely geometries at plane specified by one non-None value of x,y,z. Parameters ---------- @@ -2028,7 +2719,6 @@ def intersections_plane( For more details refer to `Shapely's Documentaton `_. """ - if not self.intersects_plane(x, y, z): return [] return [ @@ -2112,4 +2802,4 @@ def _surface_area(self, bounds: Bound) -> float: return np.sum(individual_areas) -from .utils import GeometryType, from_shapely # noqa: E402 +from .utils import GeometryType, from_shapely, vertices_from_shapely # noqa: E402 diff --git a/tidy3d/components/geometry/mesh.py b/tidy3d/components/geometry/mesh.py index 2dc69a0b8..466e5903e 100644 --- a/tidy3d/components/geometry/mesh.py +++ b/tidy3d/components/geometry/mesh.py @@ -8,7 +8,7 @@ import numpy as np from ..base import cached_property -from ..types import Ax, Bound, Shapely +from ..types import Ax, Bound, Coordinate, MatrixReal4x4, Shapely, trimesh, TrimeshType from ..viz import add_ax_if_none, equal_aspect from ...log import log from ...exceptions import ValidationError, DataError @@ -18,19 +18,6 @@ from . import base -try: - import trimesh - - TRIMESH_AVAILABLE = True -except Exception: - TRIMESH_AVAILABLE = False - -try: - - NETWORKX_RTREE_AVAILABLE = True -except Exception: - NETWORKX_RTREE_AVAILABLE = False - class TriangleMesh(base.Geometry, ABC): """Custom surface geometry given by a triangle mesh, as in the STL file format. @@ -48,20 +35,10 @@ class TriangleMesh(base.Geometry, ABC): description="Surface mesh data.", ) - @classmethod - def _check_trimesh_library(cls): - """Check if the trimesh package is imported.""" - if not TRIMESH_AVAILABLE: - raise ImportError( - "The package 'trimesh' was not found. Please install the 'trimesh' " - "dependencies to use 'TriangleMesh'. For example: " - "pip install -r requirements/trimesh.txt." - ) - @pydantic.root_validator(pre=True) + @base.requires_trimesh def _validate_trimesh_library(cls, values): """Check if the trimesh package is imported as a validator.""" - cls._check_trimesh_library() return values @pydantic.validator("mesh_dataset", pre=True, always=True) @@ -103,6 +80,7 @@ def _check_mesh(cls, val: TriangleMeshDataset) -> TriangleMeshDataset: return val @classmethod + @base.requires_trimesh def from_stl( cls, filename: str, @@ -137,14 +115,12 @@ def from_stl( The geometry or geometry group from the file. """ - def process_single(mesh: trimesh.Trimesh) -> TriangleMesh: + def process_single(mesh: TrimeshType) -> TriangleMesh: """Process a single 'trimesh.Trimesh' using scale and origin.""" mesh.apply_scale(scale) mesh.apply_translation(origin) return cls.from_trimesh(mesh) - cls._check_trimesh_library() - scene = trimesh.load(filename, **kwargs) meshes = [] if isinstance(scene, trimesh.Trimesh): @@ -169,7 +145,7 @@ def process_single(mesh: trimesh.Trimesh) -> TriangleMesh: raise ValidationError("No solid found at 'solid_index' in the stl file.") @classmethod - def from_trimesh(cls, mesh: trimesh.Trimesh) -> TriangleMesh: + def from_trimesh(cls, mesh: TrimeshType) -> TriangleMesh: """Create a :class:`.TriangleMesh` from a ``trimesh.Trimesh`` object. Parameters @@ -218,6 +194,7 @@ def from_triangles(cls, triangles: np.ndarray) -> TriangleMesh: return TriangleMesh(mesh_dataset=mesh_dataset) @classmethod + @base.requires_trimesh def from_vertices_faces(cls, vertices: np.ndarray, faces: np.ndarray) -> TriangleMesh: """Create a :class:`.TriangleMesh` from numpy arrays containing the data of a surface mesh. The first array contains the vertices, and the second array contains @@ -240,9 +217,6 @@ def from_vertices_faces(cls, vertices: np.ndarray, faces: np.ndarray) -> Triangl The custom surface mesh geometry given by the vertices and faces provided. """ - - cls._check_trimesh_library() - vertices = np.array(vertices) faces = np.array(faces) if len(vertices.shape) != 2 or vertices.shape[1] != 3: @@ -254,14 +228,13 @@ def from_vertices_faces(cls, vertices: np.ndarray, faces: np.ndarray) -> Triangl return cls.from_triangles(trimesh.Trimesh(vertices, faces).triangles) @classmethod - def _triangles_to_trimesh(cls, triangles: np.ndarray) -> trimesh.Trimesh: + @base.requires_trimesh + def _triangles_to_trimesh(cls, triangles: np.ndarray) -> TrimeshType: """Convert an (N, 3, 3) numpy array of triangles to a ``trimesh.Trimesh``.""" - - cls._check_trimesh_library() return trimesh.Trimesh(**trimesh.triangles.to_kwargs(triangles)) @cached_property - def trimesh(self) -> trimesh.Trimesh: + def trimesh(self) -> TrimeshType: """A ``trimesh.Trimesh`` object representing the custom surface mesh geometry.""" return self._triangles_to_trimesh(self.triangles) @@ -295,6 +268,33 @@ def bounds(self) -> Bound: return ((-inf, -inf, -inf), (inf, inf, inf)) return self.trimesh.bounds + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + section = self.trimesh.section(plane_origin=origin, plane_normal=normal) + if section is None: + return [] + path, _ = section.to_planar(to_2D=to_2D) + return path.polygons_full.tolist() + def intersections_plane( self, x: float = None, y: float = None, z: float = None ) -> List[Shapely]: @@ -327,13 +327,6 @@ def intersections_plane( mesh = self.trimesh - if not NETWORKX_RTREE_AVAILABLE: - raise ImportError( - "'TriangleMesh.intersections_plane' requires 'networkx' and 'rtree'. " - "Please install the 'trimesh' dependencies. For example: " - "pip install -r requirements/trimesh.txt." - ) - section = mesh.section(plane_origin=origin, plane_normal=normal) if section is None: diff --git a/tidy3d/components/geometry/polyslab.py b/tidy3d/components/geometry/polyslab.py index f9ed4960d..2a746180f 100644 --- a/tidy3d/components/geometry/polyslab.py +++ b/tidy3d/components/geometry/polyslab.py @@ -11,12 +11,14 @@ from matplotlib import path from ..base import cached_property -from ..types import Axis, Bound, PlanePosition, ArrayFloat2D +from ..types import Axis, Bound, PlanePosition, ArrayFloat2D, Coordinate +from ..types import MatrixReal4x4, Shapely, trimesh from ...log import log from ...exceptions import SetupError, ValidationError from ...constants import MICROMETER, fp_eps from . import base +from . import triangulation # sampling polygon along dilation for validating polygon to be # non self-intersecting during the entire dilation process @@ -27,6 +29,9 @@ # Warn for too many divided polyslabs _COMPLEX_POLYSLAB_DIVISIONS_WARN = 100 +# Warn before triangulating large polyslabs due to inefficiency +_MAX_POLYSLAB_VERTICES_FOR_TRIANGULATION = 500 + class PolySlab(base.Planar): """Polygon extruded with optional sidewall angle along axis direction. @@ -519,6 +524,59 @@ def _move_axis_reverse(arr): inside_polygon = face_polygon.covers(point) return inside_height * inside_polygon + @base.requires_trimesh + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + if len(self.base_polygon) > _MAX_POLYSLAB_VERTICES_FOR_TRIANGULATION: + log.warning( + "Processing of PolySlabs with large numbers of vertices can be slow.", log_once=True + ) + base_triangles = triangulation.triangulate(self.base_polygon) + top_triangles = ( + base_triangles + if isclose(self.sidewall_angle, 0) + else triangulation.triangulate(self.top_polygon) + ) + + n = len(self.base_polygon) + faces = ( + [[a, b, c] for c, b, a in base_triangles] + + [[n + a, n + b, n + c] for a, b, c in top_triangles] + + [(i, (i + 1) % n, n + i) for i in range(n)] + + [((i + 1) % n, n + ((i + 1) % n), n + i) for i in range(n)] + ) + + x = np.hstack((self.base_polygon[:, 0], self.top_polygon[:, 0])) + y = np.hstack((self.base_polygon[:, 1], self.top_polygon[:, 1])) + z = np.hstack((np.full(n, self.slab_bounds[0]), np.full(n, self.slab_bounds[1]))) + vertices = np.vstack(self.unpop_axis(z, (x, y), self.axis)).T + mesh = trimesh.Trimesh(vertices, faces) + + section = mesh.section(plane_origin=origin, plane_normal=normal) + if section is None: + return [] + path, _ = section.to_planar(to_2D=to_2D) + return path.polygons_full.tolist() + def _intersections_normal(self, z: float): """Find shapely geometries intersecting planar geometry with axis normal to slab. @@ -1465,3 +1523,34 @@ def _dilation_value_at_reference_to_coord(self, dilation: float) -> float: return z_coord + self.length_axis # bottom case return z_coord + + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + return [ + shapely.unary_union( + [ + shape + for polyslab in self.sub_polyslabs + for shape in polyslab.intersections_tilted_plane(normal, origin, to_2D) + ] + ) + ] diff --git a/tidy3d/components/geometry/primitives.py b/tidy3d/components/geometry/primitives.py index 3eb4925bb..235b2c09b 100644 --- a/tidy3d/components/geometry/primitives.py +++ b/tidy3d/components/geometry/primitives.py @@ -9,7 +9,7 @@ import shapely from ..base import cached_property -from ..types import Axis, Bound +from ..types import Axis, Bound, Coordinate, MatrixReal4x4, Shapely, trimesh from ...exceptions import SetupError, ValidationError from ...constants import MICROMETER, LARGE_NUMBER @@ -58,6 +58,47 @@ def inside( dist_z = np.abs(z - z0) return (dist_x**2 + dist_y**2 + dist_z**2) <= (self.radius**2) + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + normal = np.array(normal) + unit_normal = normal / (np.sum(normal**2) ** 0.5) + projection = np.dot(np.array(origin) - np.array(self.center), unit_normal) + if abs(projection) >= self.radius: + return [] + + radius = (self.radius**2 - projection**2) ** 0.5 + center = np.array(self.center) + projection * unit_normal + + v = np.zeros(3) + v[np.argmin(np.abs(unit_normal))] = 1 + u = np.cross(unit_normal, v) + u /= np.sum(u**2) ** 0.5 + v = np.cross(unit_normal, u) + + angles = np.linspace(0, 2 * np.pi, _N_SHAPELY_QUAD_SEGS * 4 + 1)[:-1] + circ = center + np.outer(np.cos(angles), radius * u) + np.outer(np.sin(angles), radius * v) + vertices = np.dot(np.hstack((circ, np.ones((angles.size, 1)))), to_2D.T) + return [shapely.Polygon(vertices[:, :2])] + def intersections_plane(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. @@ -182,6 +223,54 @@ def _normal_2dmaterial(self) -> Axis: raise ValidationError("'Medium2D' requires the 'Cylinder' length to be zero.") return self.axis + @base.requires_trimesh + def intersections_tilted_plane( + self, normal: Coordinate, origin: Coordinate, to_2D: MatrixReal4x4 + ) -> List[Shapely]: + """Return a list of shapely geometries at the plane specified by normal and origin. + + Parameters + ---------- + normal : Coordinate + Vector defining the normal direction to the plane. + origin : Coordinate + Vector defining the plane origin. + to_2D : MatrixReal4x4 + Transformation matrix to apply to resulting shapes. + + Returns + ------- + List[shapely.geometry.base.BaseGeometry] + List of 2D shapes that intersect plane. + For more details refer to + `Shapely's Documentaton `_. + """ + z0, (x0, y0) = self.pop_axis(self.center, self.axis) + + angles = np.linspace(0, 2 * np.pi, _N_SHAPELY_QUAD_SEGS * 4 + 1)[:-1] + x = (x0 + self.radius * np.cos(angles)).tolist() + y = (y0 + self.radius * np.sin(angles)).tolist() + n = len(x) + x.append(x0) + y.append(y0) + x = np.array(x * 2) + y = np.array(y * 2) + z = np.hstack((np.full(n + 1, z0 - self.length / 2), np.full(n + 1, z0 + self.length / 2))) + vertices = np.vstack(self.unpop_axis(z, (x, y), self.axis)).T + faces = ( + [(n, (i + 1) % n, i) for i in range(n)] + + [(1 + 2 * n, 1 + n + i, 1 + n + ((i + 1) % n)) for i in range(n)] + + [(i, (i + 1) % n, 1 + n + i) for i in range(n)] + + [((i + 1) % n, 1 + n + ((i + 1) % n), 1 + n + i) for i in range(n)] + ) + mesh = trimesh.Trimesh(vertices, faces) + + section = mesh.section(plane_origin=origin, plane_normal=normal) + if section is None: + return [] + path, _ = section.to_planar(to_2D=to_2D) + return path.polygons_full.tolist() + def _intersections_normal(self, z: float): """Find shapely geometries intersecting cylindrical geometry with axis normal to slab. diff --git a/tidy3d/components/geometry/triangulation.py b/tidy3d/components/geometry/triangulation.py new file mode 100644 index 000000000..0cb3c859f --- /dev/null +++ b/tidy3d/components/geometry/triangulation.py @@ -0,0 +1,146 @@ +from typing import List, Tuple + +from dataclasses import dataclass +import numpy as np +import shapely + +from ..types import ArrayFloat1D, ArrayFloat2D + + +@dataclass +class Vertex: + """Simple data class to hold triangulation data structures. + + Parameters + ---------- + coordinate: ArrayFloat1D + Vertex coordinate. + index : int + Vertex index in the original polygon. + is_convex : bool = False + Flag indicating whether this is a convex vertex in the polygon. + is_ear : bool = False + Flag indicating whether this is an ear of the polygon. + """ + + coordinate: ArrayFloat1D + + index: int + + is_convex: bool = False + + is_ear: bool = False + + +def update_convexity(vertices: List[Vertex], i: int) -> None: + """Update the convexity of a vertex in a polygon. + + Parameters + ---------- + vertices : List[Vertex] + Vertices of the polygon. + i : int + Index of the vertex to be updated. + """ + j = (i + 1) % len(vertices) + vertices[i].is_convex = ( + np.cross( + vertices[i].coordinate - vertices[i - 1].coordinate, + vertices[j].coordinate - vertices[i].coordinate, + ) + > 0 + ) + + +def is_inside( + vertex: ArrayFloat1D, triangle: Tuple[ArrayFloat1D, ArrayFloat1D, ArrayFloat1D] +) -> bool: + """Check if a vertex is inside a triangle. + + Parameters + ---------- + vertex : ArrayFloat1D + Vertex coordinates. + triangle : Tuple[ArrayFloat1D, ArrayFloat1D, ArrayFloat1D] + Vertices of the triangle in CCW order. + + Returns + ------- + bool: + Flag indicating if the vertex is inside the triangle. + """ + return all( + np.cross(triangle[i] - triangle[i - 1], vertex - triangle[i - 1]) > 0 for i in range(3) + ) + + +def update_ear_flag(vertices: List[Vertex], i: int) -> None: + """Update the ear flag of a vertex in a polygon. + + Parameters + ---------- + vertices : List[Vertex] + Vertices of the polygon. + i : int + Index of the vertex to be updated. + """ + h = (i - 1) % len(vertices) + j = (i + 1) % len(vertices) + triangle = (vertices[h].coordinate, vertices[i].coordinate, vertices[j].coordinate) + vertices[i].is_ear = vertices[i].is_convex and not any( + is_inside(v.coordinate, triangle) + for k, v in enumerate(vertices) + if not (v.is_convex or k == h or k == i or k == j) + ) + + +# TODO: This is an inefficient algorithm that runs in O(n^2). We should use something +# better, and probably as a compiled extension. +def triangulate(vertices: ArrayFloat2D) -> List[Tuple[int, int, int]]: + """Triangulate a simple polygon. + + Parameters + ---------- + vertices : ArrayFloat2D + Vertices of the polygon. + + Returns + ------- + List[Tuple[int, int, int]] + List of indices of the vertices of the triangles. + """ + is_ccw = shapely.LinearRing(vertices).is_ccw + vertices = [Vertex(v, i) for i, v in enumerate(vertices)] + if not is_ccw: + vertices.reverse() + + for i in range(len(vertices)): + update_convexity(vertices, i) + + for i in range(len(vertices)): + update_ear_flag(vertices, i) + + triangles = [] + + while len(vertices) > 3: + i = 0 + while i < len(vertices): + if vertices[i].is_ear: + j = (i + 1) % len(vertices) + triangles.append((vertices[i - 1].index, vertices[i].index, vertices[j].index)) + vertices.pop(i) + if len(vertices) == 3: + break + h = (i - 1) % len(vertices) + j = i % len(vertices) + if not vertices[h].is_convex: + update_convexity(vertices, h) + if not vertices[j].is_convex: + update_convexity(vertices, j) + update_ear_flag(vertices, h) + update_ear_flag(vertices, j) + else: + i += 1 + + triangles.append(tuple(v.index for v in vertices)) + return triangles diff --git a/tidy3d/components/geometry/utils.py b/tidy3d/components/geometry/utils.py index dbe9ad956..fa4d764da 100644 --- a/tidy3d/components/geometry/utils.py +++ b/tidy3d/components/geometry/utils.py @@ -1,8 +1,11 @@ """Utilities for geometry manipulation.""" from __future__ import annotations from typing import Union, Tuple +from math import isclose -from ..types import Axis, PlanePosition, Shapely +import numpy as np + +from ..types import Axis, PlanePosition, Shapely, ArrayFloat2D, MatrixReal4x4 from ...exceptions import Tidy3dError from . import base @@ -12,6 +15,7 @@ GeometryType = Union[ base.Box, + base.Transformed, base.ClipOperation, base.GeometryGroup, primitives.Sphere, @@ -80,7 +84,6 @@ def traverse_geometries(geometry: GeometryType) -> GeometryType: yield geometry -# pylint:disable=too-many-arguments def from_shapely( shape: Shapely, axis: Axis, @@ -158,3 +161,50 @@ def from_shapely( ) raise Tidy3dError(f"Shape {shape} cannot be converted to Geometry.") + + +def vertices_from_shapely(shape: Shapely) -> ArrayFloat2D: + """Iterate over the polygons of a shapely geometry returning the vertices. + + Parameters + ---------- + shape : shapely.geometry.base.BaseGeometry + Shapely primitive to have its vertices extracted. It must be a linear ring, a polygon or a + collection of any of those. + + Returns + ------- + List[Tuple[ArrayFloat2D]] + List of tuples `(exterior, *interiors)`. + """ + if shape.geom_type == "LinearRing": + return [(shape.coords[:-1],)] + if shape.geom_type == "Polygon": + return [(shape.exterior.coords[:-1],) + tuple(hole.coords[:-1] for hole in shape.interiors)] + if shape.geom_type in {"MultiPolygon", "GeometryCollection"}: + return sum(vertices_from_shapely(geo) for geo in shape.geoms) + + raise Tidy3dError(f"Shape {shape} cannot be converted to Geometry.") + + +def validate_no_transformed_polyslabs(geometry: GeometryType, transform: MatrixReal4x4 = None): + """Prevents the creation of slanted polyslabs rotated out of plane.""" + if transform is None: + transform = np.eye(4) + if isinstance(geometry, polyslab.PolySlab): + if not ( + isclose(geometry.sidewall_angle, 0) + or base.Transformed.preserves_axis(transform, geometry.axis) + ): + raise Tidy3dError( + "Slanted PolySlabs are not allowed to be rotated out of the slab plane." + ) + elif isinstance(geometry, base.Transformed): + transform = np.dot(transform, geometry.transform) + validate_no_transformed_polyslabs(geometry.geometry, transform) + elif isinstance(geometry, base.GeometryGroup): + for geo in geometry.geometries: + validate_no_transformed_polyslabs(geo, transform) + elif isinstance(geometry, base.ClipOperation): + validate_no_transformed_polyslabs(geometry.geometry_a, transform) + validate_no_transformed_polyslabs(geometry.geometry_b, transform) diff --git a/tidy3d/components/grid/mesher.py b/tidy3d/components/grid/mesher.py index 081405683..f169846ca 100644 --- a/tidy3d/components/grid/mesher.py +++ b/tidy3d/components/grid/mesher.py @@ -16,7 +16,7 @@ from ..base import Tidy3dBaseModel from ..types import Axis, ArrayFloat1D from ..structure import Structure, MeshOverrideStructure, StructureType -from ..medium import PECMedium, Medium2D +from ..medium import AnisotropicMedium, Medium2D, PECMedium from ...exceptions import SetupError, ValidationError from ...constants import C_0, fp_eps @@ -373,7 +373,13 @@ def structure_steps( min_steps = [] for structure in structures: if isinstance(structure, Structure): - if isinstance(structure.medium, (PECMedium, Medium2D)): + if isinstance(structure.medium, (PECMedium, Medium2D)) or ( + isinstance(structure.medium, AnisotropicMedium) + and structure.medium.is_comp_pec(axis) + ): + # for 2d medium, will always ignore even if not PEC; + # later, this will be handled by _grid_corrections_2dmaterials + # in simulation.py index = 1.0 else: n, k = structure.medium.eps_complex_to_nk( diff --git a/tidy3d/components/heat/__init__.py b/tidy3d/components/heat/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tidy3d/components/heat/boundary.py b/tidy3d/components/heat/boundary.py new file mode 100644 index 000000000..91db169ba --- /dev/null +++ b/tidy3d/components/heat/boundary.py @@ -0,0 +1,93 @@ +"""Defines heat material specifications""" +from __future__ import annotations + +from abc import ABC +from typing import Union + +import pydantic.v1 as pd + +from ..base import Tidy3dBaseModel +from ..bc_placement import BCPlacementType + +from ...constants import KELVIN, HEAT_FLUX, HEAT_TRANSFER_COEFF + + +class HeatBC(ABC, Tidy3dBaseModel): + """Abstract thermal boundary conditions.""" + + +class TemperatureBC(HeatBC): + """Constant temperature thermal boundary conditions. + + Example + ------- + >>> bc = TemperatureBC(temperature=300) + """ + + temperature: pd.PositiveFloat = pd.Field( + title="Temperature", + description=f"Temperature value in units of {KELVIN}.", + units=KELVIN, + ) + + +class HeatFluxBC(HeatBC): + """Constant flux thermal boundary conditions. + + Example + ------- + >>> bc = HeatFluxBC(flux=1) + """ + + flux: float = pd.Field( + title="Heat Flux", + description=f"Heat flux value in units of {HEAT_FLUX}.", + units=HEAT_FLUX, + ) + + +class ConvectionBC(HeatBC): + """Convective thermal boundary conditions. + + Example + ------- + >>> bc = ConvectionBC(ambient_temperature=300, transfer_coeff=1) + """ + + ambient_temperature: pd.PositiveFloat = pd.Field( + title="Ambient Temperature", + description=f"Ambient temperature value in units of {KELVIN}.", + units=KELVIN, + ) + + transfer_coeff: pd.NonNegativeFloat = pd.Field( + title="Heat Transfer Coefficient", + description=f"Heat flux value in units of {HEAT_TRANSFER_COEFF}.", + units=HEAT_TRANSFER_COEFF, + ) + + +HeatBoundaryConditionType = Union[TemperatureBC, HeatFluxBC, ConvectionBC] + + +class HeatBoundarySpec(Tidy3dBaseModel): + """Heat boundary conditions specification. + + Example + ------- + >>> from tidy3d import SimulationBoundary + >>> bc_spec = HeatBoundarySpec( + ... placement=SimulationBoundary(), + ... condition=ConvectionBC(ambient_temperature=300, transfer_coeff=1), + ... ) + """ + + placement: BCPlacementType = pd.Field( + title="Boundary Conditions Placement", + description="Location to apply boundary conditions.", + ) + + condition: HeatBoundaryConditionType = pd.Field( + title="Boundary Conditions", + description="Boundary conditions to apply at the selected location.", + ) diff --git a/tidy3d/components/heat/data/__init__.py b/tidy3d/components/heat/data/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tidy3d/components/heat/data/monitor_data.py b/tidy3d/components/heat/data/monitor_data.py new file mode 100644 index 000000000..8f3e4e080 --- /dev/null +++ b/tidy3d/components/heat/data/monitor_data.py @@ -0,0 +1,112 @@ +"""Monitor level data, store the DataArrays associated with a single heat monitor.""" +from __future__ import annotations +from typing import Union, Tuple, Optional + +from abc import ABC + +import pydantic.v1 as pd + +from ..monitor import TemperatureMonitor, HeatMonitorType +from ...base_sim.data.monitor_data import AbstractMonitorData +from ...data.data_array import SpatialDataArray +from ...data.dataset import TriangularGridDataset, TetrahedralGridDataset +from ...types import ScalarSymmetry, Coordinate, annotate_type +from ....constants import KELVIN + +from ....log import log + + +class HeatMonitorData(AbstractMonitorData, ABC): + """Abstract base class of objects that store data pertaining to a single :class:`HeatMonitor`.""" + + monitor: HeatMonitorType = pd.Field( + ..., + title="Monitor", + description="Monitor associated with the data.", + ) + + symmetry: Tuple[ScalarSymmetry, ScalarSymmetry, ScalarSymmetry] = pd.Field( + (0, 0, 0), + title="Symmetry", + description="Symmetry of the original simulation in x, y, and z.", + ) + + symmetry_center: Coordinate = pd.Field( + (0, 0, 0), + title="Symmetry Center", + description="Symmetry center of the original simulation in x, y, and z.", + ) + + @property + def symmetry_expanded_copy(self) -> HeatMonitorData: + """Return copy of self with symmetry applied.""" + return self.copy() + + +class TemperatureData(HeatMonitorData): + """Data associated with a :class:`TemperatureMonitor`: spatial temperature field. + + Example + ------- + >>> from tidy3d import TemperatureMonitor, SpatialDataArray + >>> import numpy as np + >>> temp_data = SpatialDataArray( + ... np.ones((2, 3, 4)), coords={"x": [0, 1], "y": [0, 1, 2], "z": [0, 1, 2, 3]} + ... ) + >>> temp_mnt = TemperatureMonitor(size=(1, 2, 3), name="temperature") + >>> temp_mnt_data = TemperatureData( + ... monitor=temp_mnt, temperature=temp_data, symmetry=(0, 1, 0), symmetry_center=(0, 0, 0) + ... ) + >>> temp_mnt_data_expanded = temp_mnt_data.symmetry_expanded_copy + """ + + monitor: TemperatureMonitor = pd.Field( + ..., title="Monitor", description="Temperature monitor associated with the data." + ) + + temperature: Optional[ + Union[SpatialDataArray, annotate_type(Union[TriangularGridDataset, TetrahedralGridDataset])] + ] = pd.Field( + ..., + title="Temperature", + description="Spatial temperature field.", + units=KELVIN, + ) + + @pd.validator("temperature", always=True) + def warn_no_data(cls, val, values): + """Warn if no data provided.""" + + mnt = values.get("monitor") + + if val is None: + log.warning( + f"No data is available for monitor '{mnt.name}'. This is typically caused by " + "monitor not intersecting any solid medium." + ) + + return val + + @property + def symmetry_expanded_copy(self) -> TemperatureData: + """Return copy of self with symmetry applied.""" + + if self.temperature is None: + return self.updated_copy(symmetry=(0, 0, 0)) + + if all(sym == 0 for sym in self.symmetry): + return self.copy() + + new_temp = self.temperature + + for dim in range(3): + # do not expand monitor with zero size along symmetry direction + # this is done because 2d unstructured data does not support this + if self.symmetry[dim] == 1 and self.monitor.size[dim] != 0: + + new_temp = new_temp.reflect(axis=dim, center=self.symmetry_center[dim]) + + return self.updated_copy(temperature=new_temp, symmetry=(0, 0, 0)) + + +HeatMonitorDataType = Union[TemperatureData] diff --git a/tidy3d/components/heat/data/sim_data.py b/tidy3d/components/heat/data/sim_data.py new file mode 100644 index 000000000..e0f5a5305 --- /dev/null +++ b/tidy3d/components/heat/data/sim_data.py @@ -0,0 +1,271 @@ +"""Defines heat simulation data class""" +from __future__ import annotations +from typing import Tuple + +import numpy as np +import pydantic.v1 as pd + +from .monitor_data import HeatMonitorDataType, TemperatureData +from ..simulation import HeatSimulation + +from ...data.dataset import UnstructuredGridDataset, TetrahedralGridDataset, TriangularGridDataset +from ...data.data_array import SpatialDataArray +from ...base_sim.data.sim_data import AbstractSimulationData +from ...types import Ax, RealFieldVal, Literal +from ...viz import equal_aspect, add_ax_if_none +from ....exceptions import DataError + + +class HeatSimulationData(AbstractSimulationData): + """Stores results of a heat simulation. + + Example + ------- + >>> from tidy3d import Medium, SolidSpec, FluidSpec, UniformUnstructuredGrid, SpatialDataArray + >>> from tidy3d import Structure, Box, UniformUnstructuredGrid, UniformHeatSource + >>> from tidy3d import StructureBoundary, TemperatureBC, TemperatureMonitor, TemperatureData + >>> from tidy3d import HeatBoundarySpec + >>> import numpy as np + >>> temp_mnt = TemperatureMonitor(size=(1, 2, 3), name="sample") + >>> heat_sim = HeatSimulation( + ... size=(3.0, 3.0, 3.0), + ... structures=[ + ... Structure( + ... geometry=Box(size=(1, 1, 1), center=(0, 0, 0)), + ... medium=Medium( + ... permittivity=2.0, heat_spec=SolidSpec( + ... conductivity=1, + ... capacity=1, + ... ) + ... ), + ... name="box", + ... ), + ... ], + ... medium=Medium(permittivity=3.0, heat_spec=FluidSpec()), + ... grid_spec=UniformUnstructuredGrid(dl=0.1), + ... sources=[UniformHeatSource(rate=1, structures=["box"])], + ... boundary_spec=[ + ... HeatBoundarySpec( + ... placement=StructureBoundary(structure="box"), + ... condition=TemperatureBC(temperature=500), + ... ) + ... ], + ... monitors=[temp_mnt], + ... ) + >>> x = [1,2] + >>> y = [2,3,4] + >>> z = [3,4,5,6] + >>> coords = dict(x=x, y=y, z=z) + >>> temp_array = SpatialDataArray(300 * np.abs(np.random.random((2,3,4))), coords=coords) + >>> temp_mnt_data = TemperatureData(monitor=temp_mnt, temperature=temp_array) + >>> heat_sim_data = HeatSimulationData( + ... simulation=heat_sim, data=[temp_mnt_data], + ... ) + """ + + simulation: HeatSimulation = pd.Field( + title="Heat Simulation", + description="Original :class:`.HeatSimulation` associated with the data.", + ) + + data: Tuple[HeatMonitorDataType, ...] = pd.Field( + ..., + title="Monitor Data", + description="List of :class:`.MonitorData` instances " + "associated with the monitors of the original :class:`.Simulation`.", + ) + + @equal_aspect + @add_ax_if_none + def plot_field( + self, + monitor_name: str, + val: RealFieldVal = "real", + scale: Literal["lin", "log"] = "lin", + structures_alpha: float = 0.2, + robust: bool = True, + vmin: float = None, + vmax: float = None, + ax: Ax = None, + **sel_kwargs, + ) -> Ax: + """Plot the data for a monitor with simulation plot overlayed. + + Parameters + ---------- + field_monitor_name : str + Name of :class:`.TemperatureMonitorData` to plot. + val : Literal['real', 'abs', 'abs^2'] = 'real' + Which part of the field to plot. + scale : Literal['lin', 'log'] + Plot in linear or logarithmic scale. + structures_alpha : float = 0.2 + Opacity of the structure permittivity. + Must be between 0 and 1 (inclusive). + robust : bool = True + If True and vmin or vmax are absent, uses the 2nd and 98th percentiles of the data + to compute the color limits. This helps in visualizing the field patterns especially + in the presence of a source. + vmin : float = None + The lower bound of data range that the colormap covers. If `None`, they are + inferred from the data and other keyword arguments. + vmax : float = None + The upper bound of data range that the colormap covers. If `None`, they are + inferred from the data and other keyword arguments. + ax : matplotlib.axes._subplots.Axes = None + matplotlib axes to plot on, if not specified, one is created. + sel_kwargs : keyword arguments used to perform `.sel()` selection in the monitor data. + These kwargs can select over the spatial dimensions (`x`, `y`, `z`), + or time dimension (`t`) if applicable. + For the plotting to work appropriately, the resulting data after selection must contain + only two coordinates with len > 1. + Furthermore, these should be spatial coordinates (`x`, `y`, or `z`). + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + monitor_data = self[monitor_name] + + if not isinstance(monitor_data, TemperatureData): + raise DataError( + f"Monitor '{monitor_name}' (type '{monitor_data.monitor.type}') is not a " + f"'TemperatureMonitor'." + ) + + if monitor_data.temperature is None: + raise DataError(f"No data to plot for monitor '{monitor_name}'.") + + field_data = self._field_component_value(monitor_data.temperature, val) + + if val == "abs^2": + field_name = "|T|², K²" + else: + field_name = "T, K" + + if scale == "log": + field_data = np.log10(np.abs(field_data)) + + cmap = "coolwarm" + + # do sel on unstructured data + # it could produce either SpatialDataArray or UnstructuredGridDatasetType + if isinstance(field_data, UnstructuredGridDataset) and len(sel_kwargs) > 0: + field_data = field_data.sel(**sel_kwargs) + + if isinstance(field_data, TetrahedralGridDataset): + raise DataError( + "Must select a two-dimensional slice of unstructured dataset for plotting" + " on a plane." + ) + + if isinstance(field_data, TriangularGridDataset): + + field_data.plot( + ax=ax, + cmap=cmap, + vmin=vmin, + vmax=vmax, + cbar_kwargs={"label": field_name}, + grid=False, + ) + + # compute parameters for structures overlay plot + axis = field_data.normal_axis + position = field_data.normal_pos + + # compute plot bounds + field_data_bounds = field_data.bounds + min_bounds = list(field_data_bounds[0]) + max_bounds = list(field_data_bounds[1]) + min_bounds.pop(axis) + max_bounds.pop(axis) + + if isinstance(field_data, SpatialDataArray): + + # interp out any monitor.size==0 dimensions + monitor = self.simulation.get_monitor_by_name(monitor_name) + thin_dims = { + "xyz"[dim]: monitor.center[dim] + for dim in range(3) + if monitor.size[dim] == 0 and "xyz"[dim] not in sel_kwargs + } + for axis, pos in thin_dims.items(): + if field_data.coords[axis].size <= 1: + field_data = field_data.sel(**{axis: pos}, method="nearest") + else: + field_data = field_data.interp(**{axis: pos}, kwargs=dict(bounds_error=True)) + + # select the extra coordinates out of the data from user-specified kwargs + for coord_name, coord_val in sel_kwargs.items(): + if field_data.coords[coord_name].size <= 1: + field_data = field_data.sel(**{coord_name: coord_val}, method=None) + else: + field_data = field_data.interp( + **{coord_name: coord_val}, kwargs=dict(bounds_error=True) + ) + + field_data = field_data.squeeze(drop=True) + non_scalar_coords = {name: c for name, c in field_data.coords.items() if c.size > 1} + + # assert the data is valid for plotting + if len(non_scalar_coords) != 2: + raise DataError( + f"Data after selection has {len(non_scalar_coords)} coordinates " + f"({list(non_scalar_coords.keys())}), " + "must be 2 spatial coordinates for plotting on plane. " + "Please add keyword arguments to `plot_monitor_data()` to select out the other coords." + ) + + spatial_coords_in_data = { + coord_name: (coord_name in non_scalar_coords) for coord_name in "xyz" + } + + if sum(spatial_coords_in_data.values()) != 2: + raise DataError( + "All coordinates in the data after selection must be spatial (x, y, z), " + f" given {non_scalar_coords.keys()}." + ) + + # get the spatial coordinate corresponding to the plane + planar_coord = [name for name, c in spatial_coords_in_data.items() if c is False][0] + axis = "xyz".index(planar_coord) + position = float(field_data.coords[planar_coord]) + + xy_coord_labels = list("xyz") + xy_coord_labels.pop(axis) + x_coord_label, y_coord_label = xy_coord_labels[0], xy_coord_labels[1] + field_data.plot( + ax=ax, + x=x_coord_label, + y=y_coord_label, + cmap=cmap, + vmin=vmin, + vmax=vmax, + robust=robust, + cbar_kwargs={"label": field_name}, + ) + + # compute plot bounds + x_coord_values = field_data.coords[x_coord_label] + y_coord_values = field_data.coords[y_coord_label] + min_bounds = (min(x_coord_values), min(y_coord_values)) + max_bounds = (max(x_coord_values), max(y_coord_values)) + + # select the cross section data + interp_kwarg = {"xyz"[axis]: position} + # plot the simulation heat conductivity + ax = self.simulation.scene.plot_structures_heat_conductivity( + cbar=False, + alpha=structures_alpha, + ax=ax, + **interp_kwarg, + ) + + # set the limits based on the xarray coordinates min and max + ax.set_xlim(min_bounds[0], max_bounds[0]) + ax.set_ylim(min_bounds[1], max_bounds[1]) + + return ax diff --git a/tidy3d/components/heat/grid.py b/tidy3d/components/heat/grid.py new file mode 100644 index 000000000..0572274dd --- /dev/null +++ b/tidy3d/components/heat/grid.py @@ -0,0 +1,119 @@ +"""Defines heat grid specifications""" +from __future__ import annotations + +from typing import Union, Tuple +import pydantic.v1 as pd + +from ..base import Tidy3dBaseModel +from ...constants import MICROMETER +from ...exceptions import ValidationError + + +class UniformUnstructuredGrid(Tidy3dBaseModel): + + """Uniform grid. + + Example + ------- + >>> heat_grid = UniformUnstructuredGrid(dl=0.1) + """ + + dl: pd.PositiveFloat = pd.Field( + ..., + title="Grid Size", + description="Grid size for uniform grid generation.", + units=MICROMETER, + ) + + min_edges_per_circumference: pd.PositiveFloat = pd.Field( + 15, + title="Minimum Edges per Circumference", + description="Enforced minimum number of mesh segments per circumference of an object. " + "Applies to :class:`Cylinder` and :class:`Sphere`, for which the circumference " + "is taken as 2 * pi * radius.", + ) + + min_edges_per_side: pd.PositiveFloat = pd.Field( + 2, + title="Minimum Edges per Side", + description="Enforced minimum number of mesh segments per any side of an object.", + ) + + non_refined_structures: Tuple[str, ...] = pd.Field( + (), + title="Structures Without Refinement", + description="List of structures for which ``min_edges_per_circumference`` and " + "``min_edges_per_side`` will not be enforced. The original ``dl`` is used instead.", + ) + + +class DistanceUnstructuredGrid(Tidy3dBaseModel): + """Adaptive grid based on distance to material interfaces. Currently not recommended for larger + simulations. + + Example + ------- + >>> heat_grid = DistanceUnstructuredGrid( + ... dl_interface=0.1, + ... dl_bulk=1, + ... distance_interface=0.3, + ... distance_bulk=2, + ... ) + """ + + dl_interface: pd.PositiveFloat = pd.Field( + ..., + title="Interface Grid Size", + description="Grid size near material interfaces.", + units=MICROMETER, + ) + + dl_bulk: pd.PositiveFloat = pd.Field( + ..., + title="Bulk Grid Size", + description="Grid size away from material interfaces.", + units=MICROMETER, + ) + + distance_interface: pd.NonNegativeFloat = pd.Field( + ..., + title="Interface Distance", + description="Distance from interface within which ``dl_interface`` is enforced." + "Typically the same as ``dl_interface`` or its multiple.", + units=MICROMETER, + ) + + distance_bulk: pd.NonNegativeFloat = pd.Field( + ..., + title="Bulk Distance", + description="Distance from interface outside of which ``dl_bulk`` is enforced." + "Typically twice of ``dl_bulk`` or its multiple. Use larger values for a smoother " + "transition from ``dl_interface`` to ``dl_bulk``.", + units=MICROMETER, + ) + + sampling: pd.PositiveFloat = pd.Field( + 100, + title="Surface Sampling", + description="An internal advanced parameter that defines number of sampling points per " + "surface when computing distance values.", + ) + + non_refined_structures: Tuple[str, ...] = pd.Field( + (), + title="Structures Without Refinement", + description="List of structures for which ``dl_interface`` will not be enforced. " + "``dl_bulk`` is used instead.", + ) + + @pd.validator("distance_bulk", always=True) + def names_exist_bcs(cls, val, values): + """Error if distance_bulk is less than distance_interface""" + distance_interface = values.get("distance_interface") + if distance_interface > val: + raise ValidationError("'distance_bulk' cannot be smaller than 'distance_interface'.") + + return val + + +HeatGridType = Union[UniformUnstructuredGrid, DistanceUnstructuredGrid] diff --git a/tidy3d/components/heat/monitor.py b/tidy3d/components/heat/monitor.py new file mode 100644 index 000000000..1fd722a48 --- /dev/null +++ b/tidy3d/components/heat/monitor.py @@ -0,0 +1,45 @@ +"""Objects that define how data is recorded from simulation.""" +from abc import ABC +import pydantic.v1 as pd +from typing import Union + +from ..types import ArrayFloat1D +from ..base_sim.monitor import AbstractMonitor + + +BYTES_REAL = 4 + + +class HeatMonitor(AbstractMonitor, ABC): + """Abstract base class for heat monitors.""" + + +class TemperatureMonitor(HeatMonitor): + """Temperature monitor.""" + + unstructured: bool = pd.Field( + False, + title="Unstructured Grid", + description="Return data on the original unstructured grid.", + ) + + conformal: bool = pd.Field( + False, + title="Conformal Monitor Meshing", + description="If ``True`` the heat simulation mesh will conform to the monitor's geometry. " + "While this can be set for both Cartesian and unstructured monitors, it bears higher " + "significance for the latter ones. Effectively, setting ``conformal = True`` for " + "unstructured monitors (``unstructured = True``) ensures that returned temperature values " + "will not be obtained by interpolation during postprocessing but rather directly " + "transferred from the computational grid.", + ) + + def storage_size(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Size of monitor storage given the number of points after discretization.""" + # stores 1 real number per grid cell, per time step, per field + num_steps = self.num_steps(tmesh) + return BYTES_REAL * num_steps * num_cells * len(self.fields) + + +# types of monitors that are accepted by heat simulation +HeatMonitorType = Union[TemperatureMonitor] diff --git a/tidy3d/components/heat/simulation.py b/tidy3d/components/heat/simulation.py new file mode 100644 index 000000000..b3608614f --- /dev/null +++ b/tidy3d/components/heat/simulation.py @@ -0,0 +1,835 @@ +"""Defines heat simulation class""" +from __future__ import annotations + +from typing import Tuple, List, Dict +from matplotlib import cm + +import pydantic.v1 as pd + +from .boundary import TemperatureBC, HeatFluxBC, ConvectionBC +from .boundary import HeatBoundarySpec +from .source import HeatSourceType, UniformHeatSource +from .monitor import HeatMonitorType +from .grid import HeatGridType +from .viz import HEAT_BC_COLOR_TEMPERATURE, HEAT_BC_COLOR_FLUX, HEAT_BC_COLOR_CONVECTION +from .viz import plot_params_heat_bc, plot_params_heat_source, HEAT_SOURCE_CMAP + +from ..base_sim.simulation import AbstractSimulation +from ..base import cached_property +from ..types import Ax, Shapely, TYPE_TAG_STR, ScalarSymmetry, Bound +from ..viz import add_ax_if_none, equal_aspect, PlotParams +from ..structure import Structure +from ..geometry.base import Box, GeometryGroup +from ..geometry.primitives import Sphere, Cylinder +from ..geometry.polyslab import PolySlab +from ..scene import Scene + +from ..bc_placement import StructureBoundary, StructureStructureInterface +from ..bc_placement import StructureSimulationBoundary, SimulationBoundary +from ..bc_placement import MediumMediumInterface + +from ...exceptions import SetupError +from ...constants import inf, VOLUMETRIC_HEAT_RATE + +from ...log import log + +HEAT_BACK_STRUCTURE_STR = "<<>>" + +HeatSingleGeometryType = (Box, Cylinder, Sphere, PolySlab) + + +class HeatSimulation(AbstractSimulation): + """Contains all information about heat simulation. + + Example + ------- + >>> from tidy3d import Medium, SolidSpec, FluidSpec, UniformUnstructuredGrid, TemperatureMonitor + >>> heat_sim = HeatSimulation( + ... size=(3.0, 3.0, 3.0), + ... structures=[ + ... Structure( + ... geometry=Box(size=(1, 1, 1), center=(0, 0, 0)), + ... medium=Medium( + ... permittivity=2.0, heat_spec=SolidSpec( + ... conductivity=1, + ... capacity=1, + ... ) + ... ), + ... name="box", + ... ), + ... ], + ... medium=Medium(permittivity=3.0, heat_spec=FluidSpec()), + ... grid_spec=UniformUnstructuredGrid(dl=0.1), + ... sources=[UniformHeatSource(rate=1, structures=["box"])], + ... boundary_spec=[ + ... HeatBoundarySpec( + ... placement=StructureBoundary(structure="box"), + ... condition=TemperatureBC(temperature=500), + ... ) + ... ], + ... monitors=[TemperatureMonitor(size=(1, 2, 3), name="sample")], + ... ) + """ + + boundary_spec: Tuple[HeatBoundarySpec, ...] = pd.Field( + (), + title="Boundary Condition Specifications", + description="List of boundary condition specifications.", + ) + + sources: Tuple[HeatSourceType, ...] = pd.Field( + (), + title="Heat Sources", + description="List of heat sources.", + ) + + monitors: Tuple[HeatMonitorType, ...] = pd.Field( + (), + title="Monitors", + description="Monitors in the simulation.", + ) + + grid_spec: HeatGridType = pd.Field( + title="Grid Specification", + description="Grid specification for heat simulation.", + discriminator=TYPE_TAG_STR, + ) + + symmetry: Tuple[ScalarSymmetry, ScalarSymmetry, ScalarSymmetry] = pd.Field( + (0, 0, 0), + title="Symmetries", + description="Tuple of integers defining reflection symmetry across a plane " + "bisecting the simulation domain normal to the x-, y-, and z-axis " + "at the simulation center of each axis, respectively. " + "Each element can be ``0`` (symmetry off) or ``1`` (symmetry on).", + ) + + @pd.validator("structures", always=True) + def check_unsupported_geometries(cls, val): + """Error if structures contain unsupported yet geometries.""" + for structure in val: + if isinstance(structure.geometry, GeometryGroup): + geometries = structure.geometry.geometries + else: + geometries = [structure.geometry] + for geom in geometries: + if isinstance(geom, (GeometryGroup)): + raise SetupError( + "'HeatSimulation' does not currently support recursive 'GeometryGroup's." + ) + if not isinstance(geom, HeatSingleGeometryType): + geom_names = [f"'{cl.__name__}'" for cl in HeatSingleGeometryType] + raise SetupError( + "'HeatSimulation' does not currently support geometries of type " + f"'{geom.type}'. Allowed geometries are " + f"{', '.join(geom_names)}, " + "and non-recursive 'GeometryGroup'." + ) + return val + + @pd.validator("size", always=True) + def check_zero_dim_domain(cls, val, values): + """Error if heat domain have zero dimensions.""" + + if any(length == 0 for length in val): + raise SetupError( + "'HeatSimulation' does not currently support domains with dimensions of zero size." + ) + + return val + + @pd.validator("boundary_spec", always=True) + def names_exist_bcs(cls, val, values): + """Error if boundary conditions point to non-existing structures/media.""" + + structures = values.get("structures") + structures_names = {s.name for s in structures} + mediums_names = {s.medium.name for s in structures} + mediums_names.add(values.get("medium").name) + + for bc_ind, bc_spec in enumerate(val): + bc_place = bc_spec.placement + if isinstance(bc_place, (StructureBoundary, StructureSimulationBoundary)): + if bc_place.structure not in structures_names: + raise SetupError( + f"Structure '{bc_place.structure}' provided in " + f"'boundary_spec[{bc_ind}].placement' (type '{bc_place.type}')" + "is not found among simulation structures." + ) + if isinstance(bc_place, (StructureStructureInterface)): + for struct_name in bc_place.structures: + if struct_name and struct_name not in structures_names: + raise SetupError( + f"Structure '{struct_name}' provided in " + f"'boundary_spec[{bc_ind}].placement' (type '{bc_place.type}') " + "is not found among simulation structures." + ) + if isinstance(bc_place, (MediumMediumInterface)): + for med_name in bc_place.mediums: + if med_name not in mediums_names: + raise SetupError( + f"Material '{med_name}' provided in " + f"'boundary_spec[{bc_ind}].placement' (type '{bc_place.type}') " + "is not found among simulation mediums." + ) + return val + + @pd.validator("grid_spec", always=True) + def names_exist_grid_spec(cls, val, values): + """Warn if UniformUnstructuredGrid points at a non-existing structure.""" + + structures = values.get("structures") + structures_names = {s.name for s in structures} + + for structure_name in val.non_refined_structures: + if structure_name not in structures_names: + log.warning( + f"Structure '{structure_name}' listed as a non-refined structure in " + "'HeatSimulation.grid_spec' is not present in 'HeatSimulation.structures'" + ) + + return val + + @pd.validator("sources", always=True) + def names_exist_sources(cls, val, values): + """Error if a heat source point to non-existing structures.""" + structures = values.get("structures") + structures_names = {s.name for s in structures} + + for source in val: + for name in source.structures: + if name not in structures_names: + raise SetupError( + f"Structure '{name}' provided in a '{source.type}' " + "is not found among simulation structures." + ) + return val + + @equal_aspect + @add_ax_if_none + def plot_heat_conductivity( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + alpha: float = None, + source_alpha: float = None, + monitor_alpha: float = None, + colorbar: str = "conductivity", + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """Plot each of simulation's components 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. + alpha : float = None + Opacity of the structures being plotted. + Defaults to the structure default alpha. + source_alpha : float = None + Opacity of the sources. If ``None``, uses Tidy3d default. + monitor_alpha : float = None + Opacity of the monitors. If ``None``, uses Tidy3d default. + colorbar: str = "conductivity" + Display colorbar for thermal conductivity ("conductivity") or heat source rate + ("source"). + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + + cbar_cond = colorbar == "conductivity" + + ax = self.scene.plot_heat_conductivity( + ax=ax, x=x, y=y, z=z, cbar=cbar_cond, alpha=alpha, hlim=hlim, vlim=vlim + ) + ax = self.plot_sources(ax=ax, x=x, y=y, z=z, alpha=source_alpha, hlim=hlim, vlim=vlim) + ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, alpha=monitor_alpha, hlim=hlim, vlim=vlim) + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + + if colorbar == "source": + self._add_heat_source_cbar(ax=ax) + return ax + + @equal_aspect + @add_ax_if_none + def plot_boundaries( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + ) -> Ax: + """Plot each of simulation's boundary conditions 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. + """ + + # get structure list + structures = [self.simulation_structure] + structures += list(self.structures) + + # construct slicing plane + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + center = Box.unpop_axis(position, (0, 0), axis=axis) + size = Box.unpop_axis(0, (inf, inf), axis=axis) + plane = Box(center=center, size=size) + + # get boundary conditions in the plane + boundaries = self._construct_heat_boundaries( + structures=structures, + plane=plane, + boundary_spec=self.boundary_spec, + ) + + # plot boundary conditions + for (bc_spec, shape) in boundaries: + ax = self._plot_boundary_condition(shape=shape, boundary_spec=bc_spec, ax=ax) + + # clean up the axis display + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + ax = self.add_ax_labels_lims(axis=axis, ax=ax) + ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") + + ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z) + + return ax + + def _get_bc_plot_params(self, boundary_spec: HeatBoundarySpec) -> PlotParams: + """Constructs the plot parameters for given boundary conditions.""" + + plot_params = plot_params_heat_bc + condition = boundary_spec.condition + + if isinstance(condition, TemperatureBC): + plot_params = plot_params.updated_copy(facecolor=HEAT_BC_COLOR_TEMPERATURE) + elif isinstance(condition, HeatFluxBC): + plot_params = plot_params.updated_copy(facecolor=HEAT_BC_COLOR_FLUX) + elif isinstance(condition, ConvectionBC): + plot_params = plot_params.updated_copy(facecolor=HEAT_BC_COLOR_CONVECTION) + + return plot_params + + def _plot_boundary_condition( + self, shape: Shapely, boundary_spec: HeatBoundarySpec, ax: Ax + ) -> Ax: + """Plot a structure's cross section shape for a given boundary condition.""" + plot_params_bc = self._get_bc_plot_params(boundary_spec=boundary_spec) + ax = self.plot_shape(shape=shape, plot_params=plot_params_bc, ax=ax) + return ax + + @staticmethod + def _structure_to_bc_spec_map( + plane: Box, structures: Tuple[Structure, ...], boundary_spec: Tuple[HeatBoundarySpec, ...] + ) -> Dict[str, HeatBoundarySpec]: + """Construct structure name to bc spec inverse mapping. One structure may correspond to + multiple boundary conditions.""" + + named_structures_present = {structure.name for structure in structures if structure.name} + + struct_to_bc_spec = {} + for bc_spec in boundary_spec: + bc_place = bc_spec.placement + if ( + isinstance(bc_place, (StructureBoundary, StructureSimulationBoundary)) + and bc_place.structure in named_structures_present + ): + if bc_place.structure in struct_to_bc_spec: + struct_to_bc_spec[bc_place.structure] += [bc_spec] + else: + struct_to_bc_spec[bc_place.structure] = [bc_spec] + + if isinstance(bc_place, StructureStructureInterface): + for structure in bc_place.structures: + if structure in named_structures_present: + if structure in struct_to_bc_spec: + struct_to_bc_spec[structure] += [bc_spec] + else: + struct_to_bc_spec[structure] = [bc_spec] + + if isinstance(bc_place, SimulationBoundary): + struct_to_bc_spec[HEAT_BACK_STRUCTURE_STR] = [bc_spec] + + return struct_to_bc_spec + + @staticmethod + def _medium_to_bc_spec_map( + plane: Box, structures: Tuple[Structure, ...], boundary_spec: Tuple[HeatBoundarySpec, ...] + ) -> Dict[str, HeatBoundarySpec]: + """Construct medium name to bc spec inverse mapping. One medium may correspond to + multiple boundary conditions.""" + + named_mediums_present = { + structure.medium.name for structure in structures if structure.medium.name + } + + med_to_bc_spec = {} + for bc_spec in boundary_spec: + bc_place = bc_spec.placement + if isinstance(bc_place, MediumMediumInterface): + for med in bc_place.mediums: + if med in named_mediums_present: + if med in med_to_bc_spec: + med_to_bc_spec[med] += [bc_spec] + else: + med_to_bc_spec[med] = [bc_spec] + + return med_to_bc_spec + + @staticmethod + def _construct_forward_boundaries( + shapes: Tuple[Tuple[str, str, Shapely, Tuple[float, float, float, float]], ...], + struct_to_bc_spec: Dict[str, HeatBoundarySpec], + med_to_bc_spec: Dict[str, HeatBoundarySpec], + background_structure_shape: Shapely, + ) -> Tuple[Tuple[HeatBoundarySpec, Shapely], ...]: + """Construct Simulation, StructureSimulation, Structure, and MediumMedium boundaries.""" + + # forward foop to take care of Simulation, StructureSimulation, Structure, + # and MediumMediums + boundaries = [] # bc_spec, structure name, shape, bounds + background_shapes = [] + for name, medium, shape, bounds in shapes: + + # intersect existing boundaries (both structure based and medium based) + for index, (_bc_spec, _name, _bdry, _bounds) in enumerate(boundaries): + + # simulation bc is overriden only by StructureSimulationBoundary + if isinstance(_bc_spec.placement, SimulationBoundary): + if name not in struct_to_bc_spec: + continue + if any( + not isinstance(bc_spec.placement, StructureSimulationBoundary) + for bc_spec in struct_to_bc_spec[name] + ): + continue + + if Box._do_not_intersect(bounds, _bounds, shape, _bdry): + continue + + diff_shape = _bdry - shape + + boundaries[index] = (_bc_spec, _name, diff_shape, diff_shape.bounds) + + # create new structure based boundary + + if name in struct_to_bc_spec: + for bc_spec in struct_to_bc_spec[name]: + + if isinstance(bc_spec.placement, StructureBoundary): + bdry = shape.exterior + bdry = bdry.intersection(background_structure_shape) + boundaries.append((bc_spec, name, bdry, bdry.bounds)) + + if isinstance(bc_spec.placement, SimulationBoundary): + boundaries.append((bc_spec, name, shape.exterior, shape.exterior.bounds)) + + if isinstance(bc_spec.placement, StructureSimulationBoundary): + bdry = background_structure_shape.exterior + bdry = bdry.intersection(shape) + boundaries.append((bc_spec, name, bdry, bdry.bounds)) + + # create new medium based boundary, and cut or merge relevant background shapes + + # loop through background_shapes (note: all background are non-intersecting or merged) + # this is similar to _filter_structures_plane but only mediums participating in BCs + # are tracked + for index, (_medium, _shape, _bounds) in enumerate(background_shapes): + + if Box._do_not_intersect(bounds, _bounds, shape, _shape): + continue + + diff_shape = _shape - shape + + # different medium, remove intersection from background shape + if medium != _medium and len(diff_shape.bounds) > 0: + background_shapes[index] = (_medium, diff_shape, diff_shape.bounds) + + # in case when there is a bc between two media + # create a new boudnary segment + for bc_spec in med_to_bc_spec[_medium.name]: + if medium.name in bc_spec.placement.mediums: + bdry = shape.exterior.intersection(_shape) + bdry = bdry.intersection(background_structure_shape) + boundaries.append((bc_spec, name, bdry, bdry.bounds)) + + # same medium, add diff shape to this shape and mark background shape for removal + # note: this only happens if this medium is listed in BCs + else: + shape = shape | diff_shape + background_shapes[index] = None + + # after doing this with all background shapes, add this shape to the background + # but only if this medium is listed in BCs + if medium.name in med_to_bc_spec: + background_shapes.append((medium, shape, shape.bounds)) + + # remove any existing background shapes that have been marked as 'None' + background_shapes = [b for b in background_shapes if b is not None] + + # filter out empty geometries + boundaries = [(bc_spec, bdry) for (bc_spec, name, bdry, _) in boundaries if bdry] + + return boundaries + + @staticmethod + def _construct_reverse_boundaries( + shapes: Tuple[Tuple[str, str, Shapely, Bound], ...], + struct_to_bc_spec: Dict[str, HeatBoundarySpec], + background_structure_shape: Shapely, + ) -> Tuple[Tuple[HeatBoundarySpec, Shapely], ...]: + """Construct StructureStructure boundaries.""" + + # backward foop to take care of StructureStructure + # we do it in this way because we define the boundary between + # two overlapping structures A and B, where A comes before B, as + # boundary(B) intersected by A + # So, in this loop as we go backwards through the structures we: + # - (1) when come upon B, create boundary(B) + # - (2) cut away from it by other structures + # - (3) when come upon A, intersect it with A and mark it as complete, + # that is, no more further modifications + boundaries_reverse = [] + + for name, _, shape, bounds in shapes[:0:-1]: + + minx, miny, maxx, maxy = bounds + + # intersect existing boundaries + for index, (_bc_spec, _name, _bdry, _bounds, _completed) in enumerate( + boundaries_reverse + ): + + if not _completed: + + if Box._do_not_intersect(bounds, _bounds, shape, _bdry): + continue + + # event (3) from above + if name in _bc_spec.placement.structures: + new_bdry = _bdry.intersection(shape) + boundaries_reverse[index] = ( + _bc_spec, + _name, + new_bdry, + new_bdry.bounds, + True, + ) + + # event (2) from above + else: + new_bdry = _bdry - shape + boundaries_reverse[index] = ( + _bc_spec, + _name, + new_bdry, + new_bdry.bounds, + _completed, + ) + + # create new boundary (event (1) from above) + if name in struct_to_bc_spec: + for bc_spec in struct_to_bc_spec[name]: + if isinstance(bc_spec.placement, StructureStructureInterface): + bdry = shape.exterior + bdry = bdry.intersection(background_structure_shape) + boundaries_reverse.append((bc_spec, name, bdry, bdry.bounds, False)) + + # filter and append completed boundaries to main list + filtered_boundaries = [] + for bc_spec, _, bdry, _, is_completed in boundaries_reverse: + if bdry and is_completed: + filtered_boundaries.append((bc_spec, bdry)) + + return filtered_boundaries + + @staticmethod + def _construct_heat_boundaries( + structures: List[Structure], + plane: Box, + boundary_spec: List[HeatBoundarySpec], + ) -> List[Tuple[HeatBoundarySpec, Shapely]]: + """Compute list of boundary lines to plot on plane. + + Parameters + ---------- + structures : List[:class:`.Structure`] + list of structures to filter on the plane. + plane : :class:`.Box` + target plane. + boundary_spec : List[HeatBoundarySpec] + list of boundary conditions associated with structures. + + Returns + ------- + List[Tuple[:class:`.HeatBoundarySpec`, shapely.geometry.base.BaseGeometry]] + List of boundary lines and boundary conditions on the plane after merging. + """ + + # get structures in the plane and present named structures and media + shapes = [] # structure name, structure medium, shape, bounds + for structure in structures: + + # get list of Shapely shapes that intersect at the plane + shapes_plane = plane.intersections_with(structure.geometry) + + # append each of them and their medium information to the list of shapes + for shape in shapes_plane: + shapes.append((structure.name, structure.medium, shape, shape.bounds)) + + background_structure_shape = shapes[0][2] + + # construct an inverse mapping structure -> bc for present structures + struct_to_bc_spec = HeatSimulation._structure_to_bc_spec_map( + plane=plane, structures=structures, boundary_spec=boundary_spec + ) + + # construct an inverse mapping medium -> bc for present mediums + med_to_bc_spec = HeatSimulation._medium_to_bc_spec_map( + plane=plane, structures=structures, boundary_spec=boundary_spec + ) + + # construct boundaries in 2 passes: + + # 1. forward foop to take care of Simulation, StructureSimulation, Structure, + # and MediumMediums + boundaries = HeatSimulation._construct_forward_boundaries( + shapes=shapes, + struct_to_bc_spec=struct_to_bc_spec, + med_to_bc_spec=med_to_bc_spec, + background_structure_shape=background_structure_shape, + ) + + # 2. reverse loop: construct structure-structure boundary + struct_struct_boundaries = HeatSimulation._construct_reverse_boundaries( + shapes=shapes, + struct_to_bc_spec=struct_to_bc_spec, + background_structure_shape=background_structure_shape, + ) + + return boundaries + struct_struct_boundaries + + @equal_aspect + @add_ax_if_none + def plot_sources( + self, + x: float = None, + y: float = None, + z: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + alpha: float = None, + ax: Ax = None, + ) -> Ax: + """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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + alpha : float = None + Opacity of the sources, If ``None`` uses Tidy3d default. + 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. + """ + + # background can't have source, so no need to add background structure + structures = self.structures + + # alpha is None just means plot without any transparency + if alpha is None: + alpha = 1 + + if alpha <= 0: + return ax + + # distribute source where there are assigned + structure_source_map = {} + for source in self.sources: + for name in source.structures: + structure_source_map[name] = source + + source_list = [structure_source_map.get(structure.name, None) for structure in structures] + + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + center = Box.unpop_axis(position, (0, 0), axis=axis) + size = Box.unpop_axis(0, (inf, inf), axis=axis) + plane = Box(center=center, size=size) + + source_shapes = self.scene._filter_structures_plane( + structures=structures, plane=plane, property_list=source_list + ) + + source_min, source_max = self.source_bounds + for (source, shape) in source_shapes: + if source is not None: + ax = self._plot_shape_structure_source( + alpha=alpha, + source=source, + source_min=source_min, + source_max=source_max, + shape=shape, + ax=ax, + ) + + # clean up the axis display + axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) + ax = self.add_ax_labels_lims(axis=axis, ax=ax) + ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") + + ax = Scene._set_plot_bounds(bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z) + return ax + + def _add_heat_source_cbar(self, ax: Ax): + """Add colorbar for heat sources.""" + source_min, source_max = self.source_bounds + self.scene._add_cbar( + vmin=source_min, + vmax=source_max, + label=f"Volumetric heat rate ({VOLUMETRIC_HEAT_RATE})", + cmap=HEAT_SOURCE_CMAP, + ax=ax, + ) + + @cached_property + def source_bounds(self) -> Tuple[float, float]: + """Compute range of heat sources present in the simulation.""" + + rate_list = [ + source.rate for source in self.sources if isinstance(source, UniformHeatSource) + ] + rate_list.append(0) + rate_min = min(rate_list) + rate_max = max(rate_list) + return rate_min, rate_max + + def _get_structure_source_plot_params( + self, + source: HeatSourceType, + source_min: float, + source_max: float, + alpha: float = None, + ) -> PlotParams: + """Constructs the plot parameters for a given medium in simulation.plot_eps().""" + + plot_params = plot_params_heat_source + if alpha is not None: + plot_params = plot_params.copy(update={"alpha": alpha}) + + if isinstance(source, UniformHeatSource): + rate = source.rate + delta_rate = rate - source_min + delta_rate_max = source_max - source_min + 1e-5 + rate_fraction = delta_rate / delta_rate_max + cmap = cm.get_cmap(HEAT_SOURCE_CMAP) + rgba = cmap(rate_fraction) + plot_params = plot_params.copy(update={"edgecolor": rgba}) + + return plot_params + + def _plot_shape_structure_source( + self, + source: HeatSourceType, + shape: Shapely, + source_min: float, + source_max: float, + ax: Ax, + alpha: float = None, + ) -> Ax: + """Plot a structure's cross section shape for a given medium, grayscale for permittivity.""" + plot_params = self._get_structure_source_plot_params( + source=source, + source_min=source_min, + source_max=source_max, + alpha=alpha, + ) + ax = self.plot_shape(shape=shape, plot_params=plot_params, ax=ax) + return ax + + @classmethod + def from_scene(cls, scene: Scene, **kwargs) -> HeatSimulation: + """Create a simulation from a :class:.`Scene` instance. Must provide additional parameters + to define a valid simulation (for example, ``size``, ``grid_spec``, etc). + + Parameters + ---------- + scene : :class:.`Scene` + Scene containing structures information. + **kwargs + Other arguments + + Example + ------- + >>> from tidy3d import Scene, Medium, Box, Structure, UniformUnstructuredGrid + >>> box = Structure( + ... geometry=Box(center=(0, 0, 0), size=(1, 2, 3)), + ... medium=Medium(permittivity=5), + ... ) + >>> scene = Scene( + ... structures=[box], + ... medium=Medium(permittivity=3), + ... ) + >>> sim = HeatSimulation.from_scene( + ... scene=scene, + ... center=(0, 0, 0), + ... size=(5, 6, 7), + ... grid_spec=UniformUnstructuredGrid(dl=0.4), + ... ) + """ + + return cls( + structures=scene.structures, + medium=scene.medium, + **kwargs, + ) diff --git a/tidy3d/components/heat/source.py b/tidy3d/components/heat/source.py new file mode 100644 index 000000000..c47735303 --- /dev/null +++ b/tidy3d/components/heat/source.py @@ -0,0 +1,49 @@ +"""Defines heat material specifications""" +from __future__ import annotations + +from abc import ABC +from typing import Union, Tuple + +import pydantic.v1 as pd + +from .viz import plot_params_heat_source + +from ..base import cached_property +from ..base_sim.source import AbstractSource +from ..data.data_array import TimeDataArray +from ..viz import PlotParams + +from ...constants import VOLUMETRIC_HEAT_RATE + + +class HeatSource(AbstractSource, ABC): + """Abstract heat source.""" + + structures: Tuple[str, ...] = pd.Field( + title="Target Structures", + description="Names of structures where to apply heat source.", + ) + + @cached_property + def plot_params(self) -> PlotParams: + """Default parameters for plotting a Source object.""" + return plot_params_heat_source + + +class UniformHeatSource(HeatSource): + """Volumetric heat source. + + Example + ------- + >>> heat_source = UniformHeatSource(rate=1, structures=["box"]) + """ + + rate: Union[float, TimeDataArray] = pd.Field( + title="Volumetric Heat Rate", + description="Volumetric rate of heating or cooling (if negative) in units of " + f"{VOLUMETRIC_HEAT_RATE}.", + units=VOLUMETRIC_HEAT_RATE, + ) + + +HeatSourceType = Union[UniformHeatSource] diff --git a/tidy3d/components/heat/viz.py b/tidy3d/components/heat/viz.py new file mode 100644 index 000000000..335a8a9d3 --- /dev/null +++ b/tidy3d/components/heat/viz.py @@ -0,0 +1,12 @@ +""" utilities for heat solver plotting """ +from ..viz import PlotParams + +""" Constants """ + +HEAT_BC_COLOR_TEMPERATURE = "orange" +HEAT_BC_COLOR_FLUX = "green" +HEAT_BC_COLOR_CONVECTION = "brown" +HEAT_SOURCE_CMAP = "coolwarm" + +plot_params_heat_bc = PlotParams(lw=3) +plot_params_heat_source = PlotParams(edgecolor="red", lw=0, hatch="..", fill=False) diff --git a/tidy3d/components/heat_spec.py b/tidy3d/components/heat_spec.py new file mode 100644 index 000000000..4666e3227 --- /dev/null +++ b/tidy3d/components/heat_spec.py @@ -0,0 +1,51 @@ +"""Defines heat material specifications""" +from __future__ import annotations + +from abc import ABC + +import pydantic.v1 as pd + +from .types import Union +from .base import Tidy3dBaseModel +from ..constants import SPECIFIC_HEAT_CAPACITY, THERMAL_CONDUCTIVITY + + +# Liquid class +class AbstractHeatSpec(ABC, Tidy3dBaseModel): + """Abstract heat material specification.""" + + +class FluidSpec(AbstractHeatSpec): + """Fluid medium. + + Example + ------- + >>> solid = FluidSpec() + """ + + +class SolidSpec(AbstractHeatSpec): + """Solid medium. + + Example + ------- + >>> solid = SolidSpec( + ... capacity=2, + ... conductivity=3, + ... ) + """ + + capacity: pd.PositiveFloat = pd.Field( + title="Heat capacity", + description=f"Volumetric heat capacity in unit of {SPECIFIC_HEAT_CAPACITY}.", + units=SPECIFIC_HEAT_CAPACITY, + ) + + conductivity: pd.PositiveFloat = pd.Field( + title="Thermal conductivity", + description=f"Thermal conductivity of material in units of {THERMAL_CONDUCTIVITY}.", + units=THERMAL_CONDUCTIVITY, + ) + + +HeatSpecType = Union[FluidSpec, SolidSpec] diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index 47a1b1eba..160a743d2 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -14,19 +14,21 @@ from .grid.grid import Coords, Grid from .types import PoleAndResidue, Ax, FreqBound, TYPE_TAG_STR from .types import InterpMethod, Bound, ArrayComplex3D, ArrayFloat1D -from .types import Axis, TensorReal +from .types import Axis, TensorReal, Complex from .data.dataset import PermittivityDataset from .data.data_array import SpatialDataArray, ScalarFieldDataArray, DATA_ARRAY_MAP from .viz import add_ax_if_none from .geometry.base import Geometry from .validators import validate_name_str, validate_parameter_perturbation -from ..constants import C_0, pec_val, EPSILON_0, LARGE_NUMBER, fp_eps, HBAR +from ..constants import C_0, pec_val, EPSILON_0, fp_eps, HBAR from ..constants import HERTZ, CONDUCTIVITY, PERMITTIVITY, RADPERSEC, MICROMETER, SECOND +from ..constants import WATT, VOLT from ..exceptions import ValidationError, SetupError from ..log import log from .transformation import RotationType from .parameter_perturbation import ParameterPerturbation - +from .heat_spec import HeatSpecType +from .time_modulation import ModulationSpec # evaluate frequency as this number (Hz) if inf FREQ_EVAL_INF = 1e50 @@ -35,7 +37,8 @@ FILL_VALUE = "extrapolate" # cap on number of nonlinear iterations -NONLINEAR_MAX_NUMITERS = 100 +NONLINEAR_MAX_NUM_ITERS = 100 +NONLINEAR_DEFAULT_NUM_ITERS = 5 # Range for checking upper bound of Im[eps], in addition to extrema method. # The range is in unit of eV and it's in log scale. @@ -50,13 +53,13 @@ def ensure_freq_in_range(eps_model: Callable[[float], complex]) -> Callable[[flo @functools.wraps(eps_model) def _eps_model(self, frequency: float) -> complex: """New eps_model function.""" - # evaluate infs and None as FREQ_EVAL_INF is_inf_scalar = isinstance(frequency, float) and np.isinf(frequency) if frequency is None or is_inf_scalar: frequency = FREQ_EVAL_INF if isinstance(frequency, np.ndarray): + frequency = frequency.astype(float) frequency[np.where(np.isinf(frequency))] = FREQ_EVAL_INF # if frequency range not present just return original function @@ -81,54 +84,245 @@ def _eps_model(self, frequency: float) -> complex: """ Medium Definitions """ -class NonlinearSpec(ABC, Tidy3dBaseModel): - """Abstract specification for adding a nonlinearity to a medium. +class NonlinearModel(ABC, Tidy3dBaseModel): + """Abstract model for a nonlinear material response. + Used as part of a :class:`.NonlinearSpec`.""" + + def _validate_medium_type(self, medium: AbstractMedium): + """Check that the model is compatible with the medium.""" + if isinstance(medium, AbstractCustomMedium): + raise ValidationError( + f"'NonlinearModel' of class '{type(self).__name__}' is not currently supported " + f"for medium class '{type(medium).__name__}'." + ) + if medium.time_modulated: + raise ValidationError( + f"'NonlinearModel' of class '{type(self).__name__}' is not currently supported " + f"for time-modulated medium class '{type(medium).__name__}'." + ) + if not isinstance(medium, (Medium, DispersiveMedium)): + raise ValidationError( + f"'NonlinearModel' of class '{type(self).__name__}' is not currently supported " + f"for medium class '{type(medium).__name__}'." + ) + + def _validate_medium(self, medium: AbstractMedium): + """Any additional validation that depends on the medium""" + pass + + def _validate_medium_freqs(self, medium: AbstractMedium, freqs: List[pd.PositiveFloat]) -> None: + """Any additional validation that depends on the central frequencies of the sources.""" + pass + + def _get_n0( + self, + n0: complex, + medium: AbstractMedium, + freqs: List[pd.PositiveFloat], + ) -> complex: + """Get a single value for n0.""" + freqs = np.array(freqs, dtype=float) + ns, ks = medium.nk_model(freqs) + nks = ns + 1j * ks + + # n0 not specified; need to calculate it + if n0 is None: + if not len(nks): + raise SetupError( + f"Class '{type(self).__name__}' cannot determine 'n0' in the absence of " + "sources. Please either specify 'n0' or add sources to the simulation." + ) + if not all(np.isclose(nk, nks[0]) for nk in nks): + raise SetupError( + f"Class '{type(self).__name__}' cannot determine 'n0' because at the source " + f"frequencies '{freqs}' the complex refractive indices '{nks}' of the medium " + f"are not all equal. Please specify 'n0' in '{type(self).__name__}' " + "to match the complex refractive index of the medium at the desired " + "source central frequency." + ) + return nks[0] + + # now, n0 is specified; we use it, but warn if it might be inconsistent + if not all(np.isclose(nk, n0) for nk in nks): + log.warning( + f"Class '{type(self).__name__}' given 'n0={n0}'. At the source frequencies " + f"'{freqs}' the medium has complex refractive indices '{nks}'. In order " + "to obtain correct nonlinearity parameters, the provided refractive index " + "should agree with the complex refractive index at the source frequencies. " + "The provided value of 'n0' is being used; the resulting nonlinearity parameters " + "may be incorrect for those sources where the complex refractive index of the " + "medium is different from this value." + ) + return n0 - Note + @property + def complex_fields(self) -> bool: + """Whether the model uses complex fields.""" + pass + + +class NonlinearSusceptibility(NonlinearModel): + """Model for an instantaneous nonlinear chi3 susceptibility. + The expression for the instantaneous nonlinear polarization is given below. + + Note ---- - The nonlinear constitutive relation is solved iteratively; it may not converge - for strong nonlinearities. Increasing `numiters` can help with convergence. + .. math:: + + P_{NL} = \\varepsilon_0 \\chi_3 |E|^2 E + + Note + ---- + This model uses real time-domain fields, so :math:`\\chi_3` must be real. + For complex fields (e.g. when using Bloch boundary conditions), the nonlinearity + is applied separately to the real and imaginary parts, so that the above equation + holds when both E and :math:`P_{NL}` are replaced by their real or imaginary parts. + The nonlinearity is applied to the real and imaginary components separately since + each of those represents a physical field. + + Note + ---- + Different field components do not interact nonlinearly. For example, + when calculating :math:`P_{NL, x}`, we approximate :math:`|E|^2 \\approx |E_x|^2`. + This approximation is valid when the E field is predominantly polarized along one + of the x, y, or z axes. + + Example + ------- + >>> nonlinear_susceptibility = NonlinearSusceptibility(chi3=1) """ + chi3: float = pd.Field( + 0, + title="Chi3", + description="Chi3 nonlinear susceptibility.", + units=f"{MICROMETER}^2 / {VOLT}^2", + ) + numiters: pd.PositiveInt = pd.Field( - 1, + None, title="Number of iterations", - description="Number of iterations for solving nonlinear constitutive relation.", + description="Deprecated. The old usage 'nonlinear_spec=model' with 'model.numiters' " + "is deprecated and will be removed in a future release. The new usage is " + r"'nonlinear_spec=NonlinearSpec(models=\[model], num_iters=num_iters)'. Under the new " + "usage, this parameter is ignored, and 'NonlinearSpec.num_iters' is used instead.", ) @pd.validator("numiters", always=True) def _validate_numiters(cls, val): """Check that numiters is not too large.""" - if val > NONLINEAR_MAX_NUMITERS: + if val is None: + return val + if val > NONLINEAR_MAX_NUM_ITERS: raise ValidationError( - "'NonlinearSpec.numiters' must be less than " - f"{NONLINEAR_MAX_NUMITERS}, currently {val}." + "'NonlinearSusceptibility.numiters' must be less than " + f"{NONLINEAR_MAX_NUM_ITERS}, currently {val}." ) return val + @property + def complex_fields(self) -> bool: + """Whether the model uses complex fields.""" + return False -class NonlinearSusceptibility(NonlinearSpec): - """Specification adding an instantaneous nonlinear susceptibility to a medium. - The expression for the instantaneous nonlinear polarization is given below. + +class TwoPhotonAbsorption(NonlinearModel): + """Model for two-photon absorption (TPA) nonlinearity which gives an intensity-dependent + absorption of the form :math:`\\alpha = \\alpha_0 + \\beta I`. + The expression for the nonlinear polarization is given below. Note ---- .. math:: - P_{NL} = \\epsilon_0 \\chi_3 |E|^2 E + P_{NL} = -\\frac{c_0^2 \\varepsilon_0^2 n_0 \\operatorname{Re}(n_0) \\beta}{2 i \\omega} |E|^2 E Note ---- - The nonlinear constitutive relation is solved iteratively; it may not converge - for strong nonlinearities. Increasing `numiters` can help with convergence. + This frequency-domain equation is implemented in the time domain using complex-valued fields. Note ---- - For complex fields (e.g. when using Bloch boundary conditions), the nonlinearity - is applied separately to the real and imaginary parts, so that the above equation - holds when both E and :math:`P_{NL}` are replaced by their real or imaginary parts. - The nonlinearity is only applied to the real-valued fields since they are the - physical fields. + Different field components do not interact nonlinearly. For example, + when calculating :math:`P_{NL, x}`, we approximate :math:`|E|^2 \\approx |E_x|^2`. + This approximation is valid when the E field is predominantly polarized along one + of the x, y, or z axes. + + Note + ---- + The implementation is described in:: + + N. Suzuki, "FDTD Analysis of Two-Photon Absorption and Free-Carrier Absorption in Si + High-Index-Contrast Waveguides," J. Light. Technol. 25, 9 (2007). + + Example + ------- + >>> tpa_model = TwoPhotonAbsorption(beta=1) + """ + + beta: Complex = pd.Field( + 0, + title="TPA coefficient", + description="Coefficient for two-photon absorption (TPA).", + units=f"{MICROMETER} / {WATT}", + ) + + n0: Optional[Complex] = pd.Field( + None, + title="Complex linear refractive index", + description="Complex linear refractive index of the medium, computed for instance using " + "'medium.nk_model'. If not provided, it is calculated automatically using the central " + "frequencies of the simulation sources (as long as these are all equal).", + ) + + def _validate_medium_freqs(self, medium: AbstractMedium, freqs: List[pd.PositiveFloat]) -> None: + """Any validation that depends on knowing the central frequencies of the sources. + This includes passivity checking, if necessary.""" + n0 = self._get_n0(self.n0, medium, freqs) + beta = self.beta + if not medium.allow_gain: + chi_imag = np.real(beta * n0 * np.real(n0)) + if chi_imag < 0: + raise ValidationError( + "For passive medium, 'beta' in 'TwoPhotonAbsorption' must satisfy " + f"'Re(beta * n0 * Re(n0)) >= 0'. Currently, this quantity equals '{chi_imag}', " + f"and the linear index is 'n0={n0}'. To simulate gain medium, please set " + "'allow_gain=True' in the medium class. Caution: simulations containing " + "gain medium are unstable, and are likely to diverge." + ) + + def _validate_medium(self, medium: AbstractMedium): + """Check that the model is compatible with the medium.""" + # if n0 is specified, we can go ahead and validate passivity + if self.n0 is not None: + self._validate_medium_freqs(medium, []) + + @property + def complex_fields(self) -> bool: + """Whether the model uses complex fields.""" + return True + + +class KerrNonlinearity(NonlinearModel): + """Model for Kerr nonlinearity which gives an intensity-dependent refractive index + of the form :math:`n = n_0 + n_2 I`. The expression for the nonlinear polarization + is given below. + + Note + ---- + .. math:: + + P_{NL} = \\varepsilon_0 c_0 n_0 \\operatorname{Re}(n_0) n_2 |E|^2 E + + Note + ---- + The fields in this equation are complex-valued, allowing a direct implementation of the Kerr + nonlinearity. In contrast, the model :class:`.NonlinearSusceptibility` implements a + chi3 nonlinear susceptibility using real-valued fields, giving rise to Kerr nonlinearity + as well as third-harmonic generation. The relationship between the parameters is given by + :math:`n_2 = \\frac{3}{4} \\frac{1}{\\varepsilon_0 c_0 n_0 \\operatorname{Re}(n_0)} \\chi_3`. The additional + factor of :math:`\\frac{3}{4}` comes from the usage of complex-valued fields for the Kerr + nonlinearity and real-valued fields for the nonlinear susceptibility. Note ---- @@ -139,15 +333,108 @@ class NonlinearSusceptibility(NonlinearSpec): Example ------- - >>> medium = Medium(permittivity=2, nonlinear_spec=NonlinearSusceptibility(chi3=1)) + >>> kerr_model = KerrNonlinearity(n2=1) """ - chi3: float = pd.Field( - ..., title="Chi3", description="Chi3 nonlinear susceptibility.", units="um^2 / V^2" + n2: Complex = pd.Field( + 0, + title="Nonlinear refractive index", + description="Nonlinear refractive index in the Kerr nonlinearity.", + units=f"{MICROMETER}^2 / {WATT}", + ) + + n0: Optional[Complex] = pd.Field( + None, + title="Complex linear refractive index", + description="Complex linear refractive index of the medium, computed for instance using " + "'medium.nk_model'. If not provided, it is calculated automatically using the central " + "frequencies of the simulation sources (as long as these are all equal).", + ) + + def _validate_medium_freqs(self, medium: AbstractMedium, freqs: List[pd.PositiveFloat]) -> None: + """Any validation that depends on knowing the central frequencies of the sources. + This includes passivity checking, if necessary.""" + n0 = self._get_n0(self.n0, medium, freqs) + n2 = self.n2 + if not medium.allow_gain: + chi_imag = np.imag(n2 * n0 * np.real(n0)) + if chi_imag < 0: + raise ValidationError( + "For passive medium, 'n2' in 'KerrNonlinearity' must satisfy " + f"'Im(n2 * n0 * Re(n0)) >= 0'. Currently, this quantity equals '{chi_imag}', " + f"and the linear index is 'n0={n0}'. To simulate gain medium, please set " + "'allow_gain=True' in the medium class. Caution: simulations containing " + "gain medium are unstable, and are likely to diverge." + ) + + def _validate_medium(self, medium: AbstractMedium): + """Check that the model is compatible with the medium.""" + # if n0 is specified, we can go ahead and validate passivity + if self.n0 is not None: + self._validate_medium_freqs(medium, []) + + @property + def complex_fields(self) -> bool: + """Whether the model uses complex fields.""" + return True + + +NonlinearModelType = Union[NonlinearSusceptibility, TwoPhotonAbsorption, KerrNonlinearity] + + +class NonlinearSpec(ABC, Tidy3dBaseModel): + """Abstract specification for adding nonlinearities to a medium. + + Note + ---- + The nonlinear constitutive relation is solved iteratively; it may not converge + for strong nonlinearities. Increasing ``num_iters`` can help with convergence. + + Example + ------- + >>> nonlinear_susceptibility = NonlinearSusceptibility(chi3=1) + >>> nonlinear_spec = NonlinearSpec(models=[nonlinear_susceptibility]) + >>> medium = Medium(permittivity=2, nonlinear_spec=nonlinear_spec) + """ + + models: Tuple[NonlinearModelType, ...] = pd.Field( + (), + title="Nonlinear models", + description="The nonlinear models present in this nonlinear spec. " + "Nonlinear models of different types are additive. " + "Multiple nonlinear models of the same type are not allowed.", + ) + + num_iters: pd.PositiveInt = pd.Field( + NONLINEAR_DEFAULT_NUM_ITERS, + title="Number of iterations", + description="Number of iterations for solving nonlinear constitutive relation.", ) + @pd.validator("models", always=True) + def _no_duplicate_models(cls, val): + """Ensure each type of model appears at most once.""" + if val is None: + return val + models = [model.__class__ for model in val] + models_unique = set(models) + if len(models) != len(models_unique): + raise ValidationError( + "Multiple 'NonlinearModels' of the same type " + "were found in a single 'NonlinearSpec'. Please ensure that " + "each type of 'NonlinearModel' appears at most once in a single 'NonlinearSpec'." + ) + return val -NonlinearSpecType = Union[NonlinearSusceptibility] + @pd.validator("num_iters", always=True) + def _validate_num_iters(cls, val, values): + """Check that num_iters is not too large.""" + if val > NONLINEAR_MAX_NUM_ITERS: + raise ValidationError( + "'NonlinearSpec.num_iters' must be less than " + f"{NONLINEAR_MAX_NUM_ITERS}, currently {val}." + ) + return val class AbstractMedium(ABC, Tidy3dBaseModel): @@ -166,30 +453,114 @@ class AbstractMedium(ABC, Tidy3dBaseModel): False, title="Allow gain medium", description="Allow the medium to be active. Caution: " - "simulations with gain medium are unstable, and are likely to diverge." + "simulations with a gain medium are unstable, and are likely to diverge." "Simulations where 'allow_gain' is set to 'True' will still be charged even if " "diverged. Monitor data up to the divergence point will still be returned and can be " "useful in some cases.", ) - nonlinear_spec: NonlinearSpecType = pd.Field( + nonlinear_spec: Union[NonlinearSpec, NonlinearSusceptibility] = pd.Field( None, title="Nonlinear Spec", description="Nonlinear spec applied on top of the base medium properties.", ) - @pd.validator("nonlinear_spec", always=True) - def _validate_nonlinear_spec(cls, val): + modulation_spec: ModulationSpec = pd.Field( + None, + title="Modulation Spec", + description="Modulation spec applied on top of the base medium properties.", + ) + + @cached_property + def _nonlinear_models(self) -> NonlinearSpec: + """The nonlinear models in the nonlinear_spec.""" + if self.nonlinear_spec is None: + return [] + if isinstance(self.nonlinear_spec, NonlinearModel): + return [self.nonlinear_spec] + if self.nonlinear_spec.models is None: + return [] + return self.nonlinear_spec.models + + @cached_property + def _nonlinear_num_iters(self) -> pd.PositiveInt: + """The num_iters of the nonlinear_spec.""" + if self.nonlinear_spec is None: + return 0 + if isinstance(self.nonlinear_spec, NonlinearModel): + if self.nonlinear_spec.numiters is None: + return 1 # old default value for backwards compatibility + return self.nonlinear_spec.numiters + return self.nonlinear_spec.num_iters + + def _post_init_validators(self) -> None: + """Call validators taking `self` that get run after init.""" + self._validate_nonlinear_spec() + + def _validate_nonlinear_spec(self): """Check compatibility with nonlinear_spec.""" - if val is None: - return val - raise ValidationError( - f"A 'nonlinear_spec' of class {type(val)} is not " - f"currently supported for medium class {cls}." - ) + if self.__class__.__name__ == "AnisotropicMedium" and any( + comp.nonlinear_spec is not None for comp in [self.xx, self.yy, self.zz] + ): + raise ValidationError( + "Nonlinearities are not currently supported for the components " + "of an anisotropic medium." + ) + if self.nonlinear_spec is None: + return + if isinstance(self.nonlinear_spec, NonlinearModel): + log.warning( + "The API for 'nonlinear_spec' has changed. " + "The old usage 'nonlinear_spec=model' is deprecated and will be removed " + "in a future release. The new usage is " + r"'nonlinear_spec=NonlinearSpec(models=\[model])'." + ) + for model in self._nonlinear_models: + model._validate_medium_type(self) + model._validate_medium(self) + if ( + isinstance(self.nonlinear_spec, NonlinearSpec) + and isinstance(model, NonlinearSusceptibility) + and model.numiters is not None + ): + raise ValidationError( + "'NonlinearSusceptibility.numiters' is deprecated. " + "Please use 'NonlinearSpec.num_iters' instead." + ) + + heat_spec: Optional[HeatSpecType] = pd.Field( + None, + title="Heat Specification", + description="Specification of the medium heat properties. They are used for solving " + "the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for " + "investigating the influence of heat propagation on the properties of optical systems. " + "Once the temperature distribution in the system is found using ``HeatSimulation`` object, " + "``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation " + "models defined into spatially dependent custom mediums. " + "Otherwise, the ``heat_spec`` does not directly affect the running of an optical " + "``Simulation``.", + discriminator=TYPE_TAG_STR, + ) + + @pd.validator("modulation_spec", always=True) + def _validate_modulation_spec(cls, val, values): + """Check compatibility with modulation_spec.""" + nonlinear_spec = values.get("nonlinear_spec") + if val is not None and nonlinear_spec is not None: + raise ValidationError( + f"For medium class {cls}, 'modulation_spec' of class {type(val)} and " + f"'nonlinear_spec' of class {type(nonlinear_spec)} are " + "not simultaneously supported." + ) + return val _name_validator = validate_name_str() + @cached_property + def time_modulated(self) -> bool: + """Whether any component of the medium is time modulated.""" + return self.modulation_spec is not None and self.modulation_spec.applied_modulation + @abstractmethod def eps_model(self, frequency: float) -> complex: """Complex-valued permittivity as a function of frequency. @@ -323,7 +694,7 @@ def nk_to_eps_complex(n: float, k: float = 0.0) -> complex: Returns ------- complex - Complex-valued relative permittivty. + Complex-valued relative permittivity. """ eps_real = n**2 - k**2 eps_imag = 2 * n * k @@ -437,6 +808,11 @@ def sigma_model(self, freq: float) -> complex: sigma = (eps_inf - eps_complex) * 1j * omega * EPSILON_0 return sigma + @cached_property + def is_pec(self): + """Whether the medium is a PEC.""" + return False + class AbstractCustomMedium(AbstractMedium, ABC): """A spatially varying medium.""" @@ -594,6 +970,16 @@ class PECMedium(AbstractMedium): To avoid confusion from duplicate PECs, must import ``tidy3d.PEC`` instance directly. """ + @pd.validator("modulation_spec", always=True) + def _validate_modulation_spec(cls, val): + """Check compatibility with modulation_spec.""" + if val is not None: + raise ValidationError( + f"A 'modulation_spec' of class {type(val)} is not " + f"currently supported for medium class {cls}." + ) + return val + def eps_model(self, frequency: float) -> complex: # return something like frequency with value of pec_val + 0j return 0j * frequency + pec_val @@ -606,6 +992,11 @@ def n_cfl(self): """ return 1.0 + @cached_property + def is_pec(self): + """Whether the medium is a PEC.""" + return True + # PEC builtin instance PEC = PECMedium(name="PEC") @@ -632,24 +1023,46 @@ class Medium(AbstractMedium): units=CONDUCTIVITY, ) - @pd.validator("nonlinear_spec", always=True) - def _validate_nonlinear_spec(cls, val): - """Check compatibility with nonlinear_spec.""" - if val is None or isinstance(val, NonlinearSusceptibility): - return val - raise ValidationError( - f"A 'nonlinear_spec' of class {type(val)} is not " - f"currently supported for medium class {cls}." - ) - @pd.validator("conductivity", always=True) def _passivity_validation(cls, val, values): """Assert passive medium if `allow_gain` is False.""" if not values.get("allow_gain") and val < 0: raise ValidationError( "For passive medium, 'conductivity' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, and are likely to diverge." + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, and are likely to diverge." + ) + return val + + @pd.validator("permittivity", always=True) + def _permittivity_modulation_validation(cls, val, values): + """Assert modulated permittivity cannot be <= 0.""" + modulation = values.get("modulation_spec") + if modulation is None or modulation.permittivity is None: + return val + + min_eps_inf = val if np.ndim(val) == 0 else np.min(np.array(val)) + if min_eps_inf - modulation.permittivity.max_modulation <= 0: + raise ValidationError( + "The minimum permittivity value with modulation applied was found to be negative." + ) + return val + + @pd.validator("conductivity", always=True) + def _passivity_modulation_validation(cls, val, values): + """Assert passive medium if `allow_gain` is False.""" + modulation = values.get("modulation_spec") + if modulation is None or modulation.conductivity is None: + return val + + min_sigma = val if np.ndim(val) == 0 else np.min(np.array(val)) + if not values.get("allow_gain") and min_sigma - modulation.conductivity.max_modulation < 0: + raise ValidationError( + "For passive medium, 'conductivity' must be non-negative at any time." + "With conductivity modulation, this medium can sometimes be active. " + "Please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " + "and are likely to diverge." ) return val @@ -661,7 +1074,11 @@ def n_cfl(self): For dispersiveless medium, it equals ``sqrt(permittivity)``. """ - return np.sqrt(self.permittivity) + permittivity = self.permittivity + if self.modulation_spec is not None and self.modulation_spec.permittivity is not None: + permittivity -= self.modulation_spec.permittivity.max_modulation + n, _ = self.eps_complex_to_nk(permittivity) + return n @ensure_freq_in_range def eps_model(self, frequency: float) -> complex: @@ -725,16 +1142,6 @@ class CustomIsotropicMedium(AbstractCustomMedium, Medium): units=CONDUCTIVITY, ) - @pd.validator("nonlinear_spec", always=True) - def _validate_nonlinear_spec(cls, val): - """Check compatibility with nonlinear_spec.""" - if val is None: - return val - raise ValidationError( - f"A 'nonlinear_spec' of class {type(val)} is not " - f"currently supported for medium class {cls}." - ) - @pd.validator("permittivity", always=True) def _eps_inf_greater_no_less_than_one(cls, val): """Assert any eps_inf must be >=1""" @@ -772,8 +1179,8 @@ def _passivity_validation(cls, val, values): if not values.get("allow_gain") and np.any(val.values < 0): raise ValidationError( "For passive medium, 'conductivity' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, and are likely to diverge." + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, and are likely to diverge." ) return val @@ -785,7 +1192,11 @@ def n_cfl(self): For dispersiveless medium, it equals ``sqrt(permittivity)``. """ - return np.sqrt(np.min(self.permittivity.values)) + permittivity = np.min(self.permittivity.values) + if self.modulation_spec is not None and self.modulation_spec.permittivity is not None: + permittivity -= self.modulation_spec.permittivity.max_modulation + n, _ = self.eps_complex_to_nk(permittivity) + return n @cached_property def is_isotropic(self): @@ -830,6 +1241,14 @@ class CustomMedium(AbstractCustomMedium): >>> eps = dielectric.eps_model(200e12) """ + eps_dataset: Optional[PermittivityDataset] = pd.Field( + None, + title="Permittivity Dataset", + description="[To be deprecated] User-supplied dataset containing complex-valued " + "permittivity as a function of space. Permittivity distribution over the Yee-grid " + "will be interpolated based on ``interp_method``.", + ) + permittivity: Optional[SpatialDataArray] = pd.Field( None, title="Permittivity", @@ -846,14 +1265,6 @@ class CustomMedium(AbstractCustomMedium): units=CONDUCTIVITY, ) - eps_dataset: Optional[PermittivityDataset] = pd.Field( - None, - title="Permittivity Dataset", - description="[To be deprecated] User-supplied dataset containing complex-valued " - "permittivity as a function of space. Permittivity distribution over the Yee-grid " - "will be interpolated based on ``interp_method``.", - ) - @pd.root_validator(pre=True) def _warn_if_none(cls, values): """Warn if the data array fails to load, and return a vacuum medium.""" @@ -951,6 +1362,7 @@ def _eps_dataset_eps_inf_greater_no_less_than_one_sigma_positive(cls, val, value """Assert any eps_inf must be >=1""" if val is None: return val + modulation = values.get("modulation_spec") for comp in ["eps_xx", "eps_yy", "eps_zz"]: eps_real, sigma = CustomMedium.eps_complex_to_eps_sigma( @@ -961,17 +1373,40 @@ def _eps_dataset_eps_inf_greater_no_less_than_one_sigma_positive(cls, val, value "Permittivity at infinite frequency at any spatial point " "must be no less than one." ) + + if modulation is not None and modulation.permittivity is not None: + if np.any(eps_real.values - modulation.permittivity.max_modulation <= 0): + raise ValidationError( + "The minimum permittivity value with modulation applied " + "was found to be negative." + ) + if not values.get("allow_gain") and np.any(sigma.values < 0): raise ValidationError( "For passive medium, imaginary part of permittivity must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " + "and are likely to diverge." + ) + + if ( + not values.get("allow_gain") + and modulation is not None + and modulation.conductivity is not None + and np.any(sigma.values - modulation.conductivity.max_modulation <= 0) + ): + raise ValidationError( + "For passive medium, imaginary part of permittivity must be non-negative " + "at any time. " + "With conductivity modulation, this medium can sometimes be active. " + "Please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) return val @pd.validator("permittivity", always=True) - def _eps_inf_greater_no_less_than_one(cls, val): + def _eps_inf_greater_no_less_than_one(cls, val, values): """Assert any eps_inf must be >=1""" if val is None: return val @@ -982,6 +1417,15 @@ def _eps_inf_greater_no_less_than_one(cls, val): if np.any(val.values < 1): raise SetupError("'permittivity' must be no less than one.") + modulation = values.get("modulation_spec") + if modulation is None or modulation.permittivity is None: + return val + + if np.any(val.values - modulation.permittivity.max_modulation <= 0): + raise ValidationError( + "The minimum permittivity value with modulation applied was found to be negative." + ) + return val @pd.validator("conductivity", always=True) @@ -1000,13 +1444,36 @@ def _conductivity_non_negative_correct_shape(cls, val, values): if not values.get("allow_gain") and np.any(val.values < 0): raise ValidationError( "For passive medium, 'conductivity' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) if values["permittivity"].coords != val.coords: raise SetupError("'permittivity' and 'conductivity' must have the same coordinates.") + + return val + + @pd.validator("conductivity", always=True) + def _passivity_modulation_validation(cls, val, values): + """Assert passive medium at any time during modulation if `allow_gain` is False.""" + + # validated already when the data is supplied through `eps_dataset` + if values.get("eps_dataset"): + return val + + # permittivity defined with ``permittivity`` and ``conductivity`` + modulation = values.get("modulation_spec") + if values.get("allow_gain") or modulation is None or modulation.conductivity is None: + return val + if val is None or np.any(val.values - modulation.conductivity.max_modulation < 0): + raise ValidationError( + "For passive medium, 'conductivity' must be non-negative at any time. " + "With conductivity modulation, this medium can sometimes be active. " + "Please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " + "and are likely to diverge." + ) return val @cached_property @@ -1347,6 +1814,49 @@ def make_bound_coords(coords: np.ndarray, pt_min: float, pt_max: float) -> List[ class DispersiveMedium(AbstractMedium, ABC): """A Medium with dispersion (propagation characteristics depend on frequency)""" + @staticmethod + def _permittivity_modulation_validation(): + """Assert modulated permittivity cannot be <= 0 at any time.""" + + @pd.validator("eps_inf", allow_reuse=True, always=True) + def _validate_permittivity_modulation(cls, val, values): + """Assert modulated permittivity cannot be <= 0.""" + modulation = values.get("modulation_spec") + if modulation is None or modulation.permittivity is None: + return val + + min_eps_inf = val if np.ndim(val) == 0 else np.min(np.array(val)) + if min_eps_inf - modulation.permittivity.max_modulation <= 0: + raise ValidationError( + "The minimum permittivity value with modulation applied was found to be negative." + ) + return val + + return _validate_permittivity_modulation + + @staticmethod + def _conductivity_modulation_validation(): + """Assert passive medium at any time if not ``allow_gain``.""" + + @pd.validator("modulation_spec", allow_reuse=True, always=True) + def _validate_conductivity_modulation(cls, val, values): + """With conductivity modulation, the medium can exhibit gain during the cycle. + So `allow_gain` must be True when the conductivity is modulated. + """ + if val is None or val.conductivity is None: + return val + + if not values.get("allow_gain"): + raise ValidationError( + "For passive medium, 'conductivity' must be non-negative at any time. " + "With conductivity modulation, this medium can sometimes be active. " + "Please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, and are likely to diverge." + ) + return val + + return _validate_conductivity_modulation + @abstractmethod def _pole_residue_dict(self) -> Dict: """Dict representation of Medium as a pole-residue model.""" @@ -1365,7 +1875,11 @@ def n_cfl(self): For PoleResidue model, it equals ``sqrt(eps_inf)`` [https://ieeexplore.ieee.org/document/9082879]. """ - return np.sqrt(self.pole_residue.eps_inf) + permittivity = self.pole_residue.eps_inf + if self.modulation_spec is not None and self.modulation_spec.permittivity is not None: + permittivity -= self.modulation_spec.permittivity.max_modulation + n, _ = self.eps_complex_to_nk(permittivity) + return n @staticmethod def tuple_to_complex(value: Tuple[float, float]) -> complex: @@ -1393,7 +1907,11 @@ def n_cfl(self): For PoleResidue model, it equals ``sqrt(eps_inf)`` [https://ieeexplore.ieee.org/document/9082879]. """ - return np.sqrt(np.min(self.pole_residue.eps_inf.values)) + permittivity = np.min(self.pole_residue.eps_inf.values) + if self.modulation_spec is not None and self.modulation_spec.permittivity is not None: + permittivity -= self.modulation_spec.permittivity.max_modulation + n, _ = self.eps_complex_to_nk(permittivity) + return n @cached_property def is_isotropic(self): @@ -1488,6 +2006,9 @@ def _causality_validation(cls, val): raise SetupError("For stable medium, 'Re(a_i)' must be non-positive.") return val + _validate_permittivity_modulation = DispersiveMedium._permittivity_modulation_validation() + _validate_conductivity_modulation = DispersiveMedium._conductivity_modulation_validation() + @ensure_freq_in_range def eps_model(self, frequency: float) -> complex: """Complex-valued permittivity as a function of frequency.""" @@ -1561,6 +2082,111 @@ def to_medium(self) -> Medium: frequency_range=self.frequency_range, ) + @staticmethod + def lo_to_eps_model( + poles: Tuple[Tuple[float, float, float, float], ...], + eps_inf: pd.PositiveFloat, + frequency: float, + ) -> complex: + """Complex permittivity as a function of frequency for a given set of LO-TO coefficients. + See ``from_lo_to`` in :class:`.PoleResidue` for the detailed form of the model + and a reference paper. + + Parameters + ---------- + poles : Tuple[Tuple[float, float, float, float], ...] + The LO-TO poles, given as list of tuples of the form + (omega_LO, gamma_LO, omega_TO, gamma_TO). + eps_inf: pd.PositiveFloat + The relative permittivity at infinite frequency. + frequency: float + Frequency at which to evaluate the permittivity. + + Returns + ------- + complex + The complex permittivity of the given LO-TO model at the given frequency. + """ + omega = 2 * np.pi * frequency + eps = eps_inf + for (omega_lo, gamma_lo, omega_to, gamma_to) in poles: + eps *= omega_lo**2 - omega**2 - 1j * omega * gamma_lo + eps /= omega_to**2 - omega**2 - 1j * omega * gamma_to + return eps + + @classmethod + def from_lo_to( + cls, poles: Tuple[Tuple[float, float, float, float], ...], eps_inf: pd.PositiveFloat = 1 + ) -> PoleResidue: + """Construct a pole residue model from the LO-TO form + (longitudinal and transverse optical modes). + The LO-TO form is :math:`\\epsilon_\\infty \\prod_{i=1}^l \\frac{\\omega_{LO, i}^2 - \\omega^2 - i \\omega \\gamma_{LO, i}}{\\omega_{TO, i}^2 - \\omega^2 - i \\omega \\gamma_{TO, i}}` as given in the paper: + + M. Schubert, T. E. Tiwald, and C. M. Herzinger, + "Infrared dielectric anisotropy and phonon modes of sapphire," + Phys. Rev. B 61, 8187 (2000). + + Parameters + ---------- + poles : Tuple[Tuple[float, float, float, float], ...] + The LO-TO poles, given as list of tuples of the form + (omega_LO, gamma_LO, omega_TO, gamma_TO). + eps_inf: pd.PositiveFloat + The relative permittivity at infinite frequency. + + Returns + ------- + :class:`.PoleResidue` + The pole residue equivalent of the LO-TO form provided. + """ + + omegas_lo, gammas_lo, omegas_to, gammas_to = map(np.array, zip(*poles)) + + # discriminants of quadratic factors of denominator + discs = 2 * np.emath.sqrt((gammas_to / 2) ** 2 - omegas_to**2) + + # require nondegenerate TO poles + if len({(omega_to, gamma_to) for (_, _, omega_to, gamma_to) in poles}) != len(poles) or any( + disc == 0 for disc in discs + ): + raise ValidationError( + "Unable to construct a pole residue model " + "from an LO-TO form with degenerate TO poles. Consider adding a " + "perturbation to split the poles, or using " + "'PoleResidue.lo_to_eps_model' and fitting with the 'FastDispersionFitter'." + ) + + # roots of denominator, in pairs + roots = [] + for gamma_to, disc in zip(gammas_to, discs): + roots.append(-gamma_to / 2 + disc / 2) + roots.append(-gamma_to / 2 - disc / 2) + + # interpolants + interpolants = eps_inf * np.ones(len(roots), dtype=complex) + for i, a in enumerate(roots): + for omega_lo, gamma_lo in zip(omegas_lo, gammas_lo): + interpolants[i] *= omega_lo**2 + a**2 + a * gamma_lo + for j, a2 in enumerate(roots): + if j != i: + interpolants[i] /= a - a2 + + a_coeffs = [] + c_coeffs = [] + + for i in range(0, len(roots), 2): + if not np.isreal(roots[i]): + a_coeffs.append(roots[i]) + c_coeffs.append(interpolants[i]) + else: + a_coeffs.append(roots[i]) + a_coeffs.append(roots[i + 1]) + # factor of two from adding conjugate pole of real pole + c_coeffs.append(interpolants[i] / 2) + c_coeffs.append(interpolants[i + 1] / 2) + + return PoleResidue(eps_inf=eps_inf, poles=list(zip(a_coeffs, c_coeffs))) + @staticmethod def eV_to_angular_freq(f_eV: float): """Convert frequency in unit of eV to rad/s. @@ -1892,12 +2518,28 @@ def _passivity_validation(cls, val, values): if B < 0: raise ValidationError( "For passive medium, 'B_i' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) return val + @pd.validator("modulation_spec", always=True) + def _validate_permittivity_modulation(cls, val): + """Assert modulated permittivity cannot be <= 0.""" + + if val is None or val.permittivity is None: + return val + + min_eps_inf = 1.0 + if min_eps_inf - val.permittivity.max_modulation <= 0: + raise ValidationError( + "The minimum permittivity value with modulation applied was found to be negative." + ) + return val + + _validate_conductivity_modulation = DispersiveMedium._conductivity_modulation_validation() + def _n_model(self, frequency: float) -> complex: """Complex-valued refractive index as a function of frequency.""" @@ -2018,8 +2660,8 @@ def _passivity_validation(cls, val, values): if np.any(B < 0): raise ValidationError( "For passive medium, 'B_i' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) return val @@ -2145,12 +2787,15 @@ def _passivity_validation(cls, val, values): if del_ep < 0: raise ValidationError( "For passive medium, 'Delta epsilon_i' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) return val + _validate_permittivity_modulation = DispersiveMedium._permittivity_modulation_validation() + _validate_conductivity_modulation = DispersiveMedium._conductivity_modulation_validation() + @ensure_freq_in_range def eps_model(self, frequency: float) -> complex: """Complex-valued permittivity as a function of frequency.""" @@ -2298,8 +2943,8 @@ def _passivity_validation(cls, val, values): if not allow_gain and np.any(del_ep < 0): raise ValidationError( "For passive medium, 'Delta epsilon_i' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) return val @@ -2354,6 +2999,9 @@ class Drude(DispersiveMedium): units=(HERTZ, HERTZ), ) + _validate_permittivity_modulation = DispersiveMedium._permittivity_modulation_validation() + _validate_conductivity_modulation = DispersiveMedium._conductivity_modulation_validation() + @ensure_freq_in_range def eps_model(self, frequency: float) -> complex: """Complex-valued permittivity as a function of frequency.""" @@ -2518,12 +3166,15 @@ def _passivity_validation(cls, val, values): if del_ep < 0: raise ValidationError( "For passive medium, 'Delta epsilon_i' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) return val + _validate_permittivity_modulation = DispersiveMedium._permittivity_modulation_validation() + _validate_conductivity_modulation = DispersiveMedium._conductivity_modulation_validation() + @ensure_freq_in_range def eps_model(self, frequency: float) -> complex: """Complex-valued permittivity as a function of frequency.""" @@ -2627,8 +3278,8 @@ def _passivity_validation(cls, val, values): if not allow_gain and np.any(del_ep < 0): raise ValidationError( "For passive medium, 'Delta epsilon_i' must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, " + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, " "and are likely to diverge." ) return val @@ -2652,7 +3303,7 @@ def eps_dataarray_freq( return (eps, eps, eps) -IsotropicUniformMediumType = Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude] +IsotropicUniformMediumType = Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium] IsotropicCustomMediumType = Union[ CustomPoleResidue, CustomSellmeier, @@ -2706,6 +3357,17 @@ class AnisotropicMedium(AbstractMedium): description="This field is ignored. Please set ``allow_gain`` in each component", ) + @pd.validator("modulation_spec", always=True) + def _validate_modulation_spec(cls, val): + """Check compatibility with modulation_spec.""" + if val is not None: + raise ValidationError( + f"A 'modulation_spec' of class {type(val)} is not " + f"currently supported for medium class {cls}. " + "Please add modulation to each component." + ) + return val + @pd.root_validator(pre=True) def _ignored_fields(cls, values): """The field is ignored.""" @@ -2720,6 +3382,11 @@ def components(self) -> Dict[str, Medium]: """Dictionary of diagonal medium components.""" return dict(xx=self.xx, yy=self.yy, zz=self.zz) + @cached_property + def time_modulated(self) -> bool: + """Whether any component of the medium is time modulated.""" + return any(mat.time_modulated for mat in self.components.values()) + @cached_property def n_cfl(self): """This property computes the index of refraction related to CFL condition, so that @@ -2793,6 +3460,15 @@ def elements(self) -> Dict[str, IsotropicUniformMediumType]: """The diagonal elements of the medium as a dictionary.""" return dict(xx=self.xx, yy=self.yy, zz=self.zz) + @cached_property + def is_pec(self): + """Whether the medium is a PEC.""" + return any(self.is_comp_pec(i) for i in range(3)) + + def is_comp_pec(self, comp: Axis): + """Whether the medium is a PEC.""" + return isinstance(self.components[["xx", "yy", "zz"][comp]], PECMedium) + class FullyAnisotropicMedium(AbstractMedium): """Fully anisotropic medium including all 9 components of the permittivity and conductivity @@ -2830,6 +3506,16 @@ class FullyAnisotropicMedium(AbstractMedium): units=CONDUCTIVITY, ) + @pd.validator("modulation_spec", always=True) + def _validate_modulation_spec(cls, val): + """Check compatibility with modulation_spec.""" + if val is not None: + raise ValidationError( + f"A 'modulation_spec' of class {type(val)} is not " + f"currently supported for medium class {cls}." + ) + return val + @pd.validator("permittivity", always=True) def permittivity_spd_and_ge_one(cls, val): """Check that provided permittivity tensor is symmetric positive definite @@ -2872,8 +3558,8 @@ def _passivity_validation(cls, val, values): raise ValidationError( "For passive medium, main diagonal of provided conductivity tensor " "must be non-negative. " - "To simulate gain medium, please set 'allow_gain=True'. " - "Caution: simulations with gain medium are unstable, and are likely to diverge." + "To simulate a gain medium, please set 'allow_gain=True'. " + "Caution: simulations with a gain medium are unstable, and are likely to diverge." ) return val @@ -2940,6 +3626,9 @@ def eps_model(self, frequency: float) -> complex: """Complex-valued permittivity as a function of frequency.""" perm_diag, cond_diag, _ = self.eps_sigma_diag + if not np.isscalar(frequency): + perm_diag = perm_diag[:, None] + cond_diag = cond_diag[:, None] eps_diag = AbstractMedium.eps_sigma_to_eps_complex(perm_diag, cond_diag, frequency) return np.mean(eps_diag) @@ -3224,6 +3913,7 @@ def perturbed_copy( temperature: SpatialDataArray = None, electron_density: SpatialDataArray = None, hole_density: SpatialDataArray = None, + interp_method: InterpMethod = "linear", ) -> Union[AbstractMedium, AbstractCustomMedium]: """Sample perturbations on provided heat and/or charge data and create a custom medium. Any of ``temperature``, ``electron_density``, and ``hole_density`` can be ``None``. @@ -3238,6 +3928,9 @@ def perturbed_copy( Electron density field data. hole_density : SpatialDataArray = None Hole density field data. + interp_method : :class:`.InterpMethod`, optional + Interpolation method to obtain heat and/or charge values that are not supplied + at the Yee grids. Returns ------- @@ -3296,6 +3989,7 @@ def perturbed_copy( temperature: SpatialDataArray = None, electron_density: SpatialDataArray = None, hole_density: SpatialDataArray = None, + interp_method: InterpMethod = "linear", ) -> Union[Medium, CustomMedium]: """Sample perturbations on provided heat and/or charge data and return 'CustomMedium'. Any of temperature, electron_density, and hole_density can be 'None'. If all passed @@ -3310,6 +4004,9 @@ def perturbed_copy( Electron density field data. hole_density : SpatialDataArray = None Hole density field data. + interp_method : :class:`.InterpMethod`, optional + Interpolation method to obtain heat and/or charge values that are not supplied + at the Yee grids. Returns ------- @@ -3318,7 +4015,11 @@ def perturbed_copy( """ new_dict = self.dict( - exclude={"permittivity_perturbation", "conductivity_perturbation", "type"} + exclude={ + "permittivity_perturbation", + "conductivity_perturbation", + "type", + } ) if all(x is None for x in [temperature, electron_density, hole_density]): @@ -3342,6 +4043,7 @@ def perturbed_copy( new_dict["permittivity"] = permittivity_field new_dict["conductivity"] = conductivity_field + new_dict["interp_method"] = interp_method return CustomMedium.parse_obj(new_dict) @@ -3407,6 +4109,7 @@ def perturbed_copy( temperature: SpatialDataArray = None, electron_density: SpatialDataArray = None, hole_density: SpatialDataArray = None, + interp_method: InterpMethod = "linear", ) -> Union[PoleResidue, CustomPoleResidue]: """Sample perturbations on provided heat and/or charge data and return 'CustomPoleResidue'. Any of temperature, electron_density, and hole_density can be 'None'. If all passed @@ -3421,6 +4124,9 @@ def perturbed_copy( Electron density field data. hole_density : SpatialDataArray = None Hole density field data. + interp_method : :class:`.InterpMethod`, optional + Interpolation method to obtain heat and/or charge values that are not supplied + at the Yee grids. Returns ------- @@ -3454,6 +4160,7 @@ def perturbed_copy( new_dict["eps_inf"] = eps_inf_field new_dict["poles"] = poles_field + new_dict["interp_method"] = interp_method return CustomPoleResidue.parse_obj(new_dict) @@ -3519,10 +4226,20 @@ class Medium2D(AbstractMedium): discriminator=TYPE_TAG_STR, ) + @pd.validator("modulation_spec", always=True) + def _validate_modulation_spec(cls, val): + """Check compatibility with modulation_spec.""" + if val is not None: + raise ValidationError( + f"A 'modulation_spec' of class {type(val)} is not " + f"currently supported for medium class {cls}." + ) + return val + @classmethod def _weighted_avg( cls, meds: List[IsotropicUniformMediumType], weights: List[float] - ) -> PoleResidue: + ) -> Union[PoleResidue, PECMedium]: """Average ``meds`` with weights ``weights``.""" eps_inf = 1 poles = [] @@ -3534,7 +4251,8 @@ def _weighted_avg( pole_res = PoleResidue.from_medium(med) eps_inf += weight * (med.eps_model(np.inf) - 1) elif isinstance(med, PECMedium): - pole_res = PoleResidue.from_medium(Medium(conductivity=LARGE_NUMBER)) + # special treatment for PEC + return med else: raise ValidationError("Invalid medium type for the components of 'Medium2D'.") poles += [(a, weight * c) for (a, c) in pole_res.poles] @@ -3665,6 +4383,8 @@ def to_medium(self, thickness: float) -> Medium: :class:`.Medium` The 3D equivalent of this 2D medium. """ + if self.is_pec: + return PEC return self.to_pole_residue(thickness=thickness).to_medium() @classmethod @@ -3761,7 +4481,7 @@ def plot(self, freqs: float, ax: Ax = None) -> Ax: """Plot n, k of a :class:`.Medium` as a function of frequency.""" log.warning( "The refractive index of a 'Medium2D' is unphysical. " - "Use 'Medium2D.plot_sigma' instead to plot surface conductivty, or call " + "Use 'Medium2D.plot_sigma' instead to plot surface conductivity, or call " "'Medium2D.to_anisotropic_medium' or 'Medium2D.to_pole_residue' first " "to obtain the physical refractive index." ) @@ -3827,6 +4547,20 @@ def n_cfl(self): """ return 1.0 + @cached_property + def is_pec(self): + """Whether the medium is a PEC.""" + return any(isinstance(comp, PECMedium) for comp in self.elements.values()) + + def is_comp_pec_2d(self, comp: Axis, axis: Axis): + """Whether the medium is a PEC.""" + elements_3d = Geometry.unpop_axis( + ax_coord=Medium(), plane_coords=self.elements.values(), axis=axis + ) + return isinstance(elements_3d[comp], PECMedium) + + +PEC2D = Medium2D(ss=PEC, tt=PEC) # types of mediums that can be used in Simulation and Structures diff --git a/tidy3d/components/mode.py b/tidy3d/components/mode.py index 376e919dd..8afabe50d 100644 --- a/tidy3d/components/mode.py +++ b/tidy3d/components/mode.py @@ -1,6 +1,7 @@ """Defines specification for mode solver.""" from typing import Tuple, Union +from math import isclose import pydantic.v1 as pd import numpy as np @@ -115,9 +116,16 @@ class ModeSpec(Tidy3dBaseModel): @pd.validator("bend_axis", always=True) def bend_axis_given(cls, val, values): - """check that ``bend_axis`` is provided if ``bend_radius`` is not ``None``""" + """Check that ``bend_axis`` is provided if ``bend_radius`` is not ``None``""" if val is None and values.get("bend_radius") is not None: - raise SetupError("bend_axis must also be defined if bend_radius is defined.") + raise SetupError("'bend_axis' must also be defined if 'bend_radius' is defined.") + return val + + @pd.validator("bend_radius", always=True) + def bend_radius_not_zero(cls, val, values): + """Check that ``bend_raidus`` magnitude is not close to zero.`""" + if val and isclose(val, 0): + raise SetupError("The magnitude of 'bend_radius' must be larger than 0.") return val @pd.validator("angle_theta", allow_reuse=True, always=True) diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 7b8a126f2..b33f9dca1 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -5,120 +5,60 @@ import pydantic.v1 as pydantic import numpy as np -from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound, Numpy -from .types import Literal, Direction, Coordinate, Axis, ObsGridArray -from .geometry.base import Box -from .validators import assert_plane +from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound, Bound, Size +from .types import Literal, Direction, Coordinate, Axis, ObsGridArray, BoxSurface +from .validators import assert_plane, validate_freqs_not_empty, validate_freqs_min from .base import cached_property, Tidy3dBaseModel from .mode import ModeSpec from .apodization import ApodizationSpec -from .viz import PlotParams, plot_params_monitor, ARROW_COLOR_MONITOR, ARROW_ALPHA +from .medium import MediumType +from .viz import ARROW_COLOR_MONITOR, ARROW_ALPHA from ..constants import HERTZ, SECOND, MICROMETER, RADIAN, inf from ..exceptions import SetupError, ValidationError from ..log import log +from .base_sim.monitor import AbstractMonitor + BYTES_REAL = 4 BYTES_COMPLEX = 8 WARN_NUM_FREQS = 2000 WARN_NUM_MODES = 100 +# Field projection windowing factor that determines field decay at the edges of surface field +# projection monitors. A value of 15 leads to a decay of < 1e-3x in field amplitude. +# This number relates directly to the standard deviation of the Gaussian function which is used +# for windowing the monitor. +WINDOW_FACTOR = 15 -class Monitor(Box, ABC): - """Abstract base class for monitors.""" - name: str = pydantic.Field( - ..., - title="Name", - description="Unique name for monitor.", - min_length=1, - ) +class Monitor(AbstractMonitor): + """Abstract base class for monitors.""" interval_space: Tuple[Literal[1], Literal[1], Literal[1]] = pydantic.Field( (1, 1, 1), - title="Spatial interval", + title="Spatial Interval", description="Number of grid step intervals between monitor recordings. If equal to 1, " - "there will be no downsampling. If greater than 1, the step will be applied, but the last " - "point of the monitor grid is always included. " + "there will be no downsampling. If greater than 1, the step will be applied, but the " + "first and last point of the monitor grid are always included. " "Not all monitors support values different from 1.", ) colocate: Literal[True] = pydantic.Field( True, - title="Colocate fields", + title="Colocate Fields", description="Defines whether fields are colocated to grid cell boundaries (i.e. to the " "primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors " "and is hard-coded for other monitors depending on their specific function.", ) - @cached_property - def plot_params(self) -> PlotParams: - """Default parameters for plotting a Monitor object.""" - return plot_params_monitor - - @cached_property - def geometry(self) -> Box: - """:class:`Box` representation of monitor. - - Returns - ------- - :class:`Box` - Representation of the monitor geometry as a :class:`Box`. - """ - return Box(center=self.center, size=self.size) - @abstractmethod def storage_size(self, num_cells: int, tmesh: ArrayFloat1D) -> int: - """Size of monitor storage given the number of points after discretization. - - Parameters - ---------- - num_cells : int - Number of grid cells within the monitor after discretization by a :class:`Simulation`. - tmesh : Array - The discretized time mesh of a :class:`Simulation`. - - Returns - ------- - int - Number of bytes to be stored in monitor. - """ - - def downsample(self, arr: Numpy, axis: Axis) -> Numpy: - """Downsample a 1D array making sure to keep the first and last entries, based on the - spatial interval defined for the ``axis``. - - Parameters - ---------- - arr : Numpy - A 1D array of arbitrary type. - axis : Axis - Axis for which to select the interval_space defined for the monitor. - - Returns - ------- - Numpy - Downsampled array. - """ + """Size of monitor storage given the number of points after discretization.""" - size = len(arr) - interval = self.interval_space[axis] - # There should always be at least 3 indices for "surface" monitors. Also, if the - # size along this dim is already smaller than the interval, then don't downsample. - if size < 4 or (size - 1) <= interval: - return arr - # make sure the last index is always included - inds = np.arange(0, size, interval) - if inds[-1] != size - 1: - inds = np.append(inds, size - 1) - return arr[inds] - - def downsampled_num_cells(self, num_cells: Tuple[int, int, int]) -> Tuple[int, int, int]: - """Given a tuple of the number of cells spanned by the monitor along each dimension, - return the number of cells one would have after downsampling based on ``interval_space``. - """ - arrs = [np.arange(ncells) for ncells in num_cells] - return tuple((self.downsample(arr, axis=dim).size for dim, arr in enumerate(arrs))) + def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Size of intermediate data recorded by the monitor during a solver run.""" + return self.storage_size(num_cells=num_cells, tmesh=tmesh) class FreqMonitor(Monitor, ABC): @@ -141,12 +81,8 @@ class FreqMonitor(Monitor, ABC): "affects the normalization of the frequency-domain fields.", ) - @pydantic.validator("freqs", always=True) - def _freqs_non_empty(cls, val): - """Assert one frequency present.""" - if len(val) == 0: - raise ValidationError("'freqs' must not be empty.") - return val + _freqs_not_empty = validate_freqs_not_empty() + _freqs_lower_bound = validate_freqs_min() @pydantic.validator("freqs", always=True) def _warn_num_freqs(cls, val, values): @@ -178,14 +114,14 @@ class TimeMonitor(Monitor, ABC): start: pydantic.NonNegativeFloat = pydantic.Field( 0.0, - title="Start time", + title="Start Time", description="Time at which to start monitor recording.", units=SECOND, ) stop: pydantic.NonNegativeFloat = pydantic.Field( None, - title="Stop time", + title="Stop Time", description="Time at which to stop monitor recording. " "If not specified, record until end of simulation.", units=SECOND, @@ -193,7 +129,7 @@ class TimeMonitor(Monitor, ABC): interval: pydantic.PositiveInt = pydantic.Field( None, - title="Time interval", + title="Time Interval", description="Sampling rate of the monitor: number of time steps between each measurement. " "Set ``inverval`` to 1 for the highest possible resolution in time. " "Higher integer values downsample the data by measuring every ``interval`` time steps. " @@ -283,32 +219,19 @@ class AbstractFieldMonitor(Monitor, ABC): pydantic.PositiveInt, pydantic.PositiveInt, pydantic.PositiveInt ] = pydantic.Field( (1, 1, 1), - title="Spatial interval", + title="Spatial Interval", description="Number of grid step intervals between monitor recordings. If equal to 1, " - "there will be no downsampling. If greater than 1, the step will be applied, but the last " - "point of the monitor grid is always included.", + "there will be no downsampling. If greater than 1, the step will be applied, but the " + "first and last point of the monitor grid are always included.", ) colocate: bool = pydantic.Field( - None, - title="Colocate fields", + True, + title="Colocate Fields", description="Toggle whether fields should be colocated to grid cell boundaries (i.e. " - "primal grid nodes). Default is ``True``.", + "primal grid nodes).", ) - # TODO: remove after 2.4 - @pydantic.validator("colocate", always=True) - def warn_set_colocate(cls, val): - """If ``colocate`` not provided, set to true, but warn that behavior has changed.""" - if val is None: - log.warning( - "Default value for the field monitor 'colocate' setting has changed to " - "'True' in Tidy3D 2.4.0. All field components will be colocated to the grid " - "boundaries. Set to 'False' to get the raw fields on the Yee grid instead." - ) - return True - return val - class PlanarMonitor(Monitor, ABC): """:class:`Monitor` that has a planar geometry.""" @@ -391,6 +314,14 @@ def _warn_num_modes(cls, val, values): ) return val + def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Size of intermediate data recorded by the monitor during a solver run.""" + # Need to store all fields on the mode surface + bytes_single = BYTES_COMPLEX * num_cells * len(self.freqs) * self.mode_spec.num_modes * 6 + if self.mode_spec.precision == "double": + return 2 * bytes_single + return bytes_single + class FieldMonitor(AbstractFieldMonitor, FreqMonitor): """:class:`Monitor` that records electromagnetic fields in the frequency domain. @@ -457,7 +388,7 @@ class PermittivityMonitor(FreqMonitor): colocate: Literal[False] = pydantic.Field( False, - title="Colocate fields", + title="Colocate Fields", description="Colocation turned off, since colocated permittivity values do not have a " "physical meaning - they do not correspond to the subpixel-averaged ones.", ) @@ -466,10 +397,10 @@ class PermittivityMonitor(FreqMonitor): pydantic.PositiveInt, pydantic.PositiveInt, pydantic.PositiveInt ] = pydantic.Field( (1, 1, 1), - title="Spatial interval", + title="Spatial Interval", description="Number of grid step intervals between monitor recordings. If equal to 1, " - "there will be no downsampling. If greater than 1, the step will be applied, but the last " - "point of the monitor grid is always included.", + "there will be no downsampling. If greater than 1, the step will be applied, but the " + "first and last point of the monitor grid are always included.", ) apodization: ApodizationSpec = pydantic.Field( @@ -490,15 +421,15 @@ class SurfaceIntegrationMonitor(Monitor, ABC): normal_dir: Direction = pydantic.Field( None, - title="Normal vector orientation", + title="Normal Vector Orientation", description="Direction of the surface monitor's normal vector w.r.t. " "the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. " "Applies to surface monitors only, and defaults to ``'+'`` if not provided.", ) - exclude_surfaces: Tuple[Literal["x-", "x+", "y-", "y+", "z-", "z+"], ...] = pydantic.Field( + exclude_surfaces: Tuple[BoxSurface, ...] = pydantic.Field( None, - title="Excluded surfaces", + title="Excluded Surfaces", description="Surfaces to exclude in the integration, if a volume monitor.", ) @@ -542,6 +473,13 @@ def check_excluded_surfaces(cls, values): ) return values + def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Size of intermediate data recorded by the monitor during a solver run.""" + # Need to store all fields on the integration surface. Frequency-domain monitors store at + # all frequencies, time domain at the current time step only. + num_sample = len(getattr(self, "freqs", [0])) + return BYTES_COMPLEX * num_cells * num_sample * 6 + class AbstractFluxMonitor(SurfaceIntegrationMonitor, ABC): """:class:`Monitor` that records flux during the solver run.""" @@ -615,7 +553,7 @@ class ModeMonitor(AbstractModeMonitor): colocate: Literal[False] = pydantic.Field( False, - title="Colocate fields", + title="Colocate Fields", description="Defines whether fields are colocated to grid cell boundaries (i.e. to the " "primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors " "and is hard-coded for other monitors depending on their specific function.", @@ -644,21 +582,24 @@ class ModeSolverMonitor(AbstractModeMonitor): direction: Direction = pydantic.Field( "+", - title="Propagation direction", + title="Propagation Direction", description="Direction of waveguide mode propagation along the axis defined by its normal " "dimension.", ) colocate: bool = pydantic.Field( True, - title="Colocate fields", + title="Colocate Fields", description="Toggle whether fields should be colocated to grid cell boundaries (i.e. " - "primal grid nodes). Default is ``True``.", + "primal grid nodes).", ) def storage_size(self, num_cells: int, tmesh: int) -> int: """Size of monitor storage given the number of points after discretization.""" - return 6 * BYTES_COMPLEX * num_cells * len(self.freqs) * self.mode_spec.num_modes + bytes_single = 6 * BYTES_COMPLEX * num_cells * len(self.freqs) * self.mode_spec.num_modes + if self.mode_spec.precision == "double": + return 2 * bytes_single + return bytes_single class FieldProjectionSurface(Tidy3dBaseModel): @@ -667,13 +608,13 @@ class FieldProjectionSurface(Tidy3dBaseModel): monitor: FieldMonitor = pydantic.Field( ..., - title="Field monitor", + title="Field Monitor", description=":class:`.FieldMonitor` on which near fields will be sampled and integrated.", ) normal_dir: Direction = pydantic.Field( ..., - title="Normal vector orientation", + title="Normal Vector Orientation", description=":class:`.Direction` of the surface monitor's normal vector w.r.t.\ the positive x, y or z unit vectors. Must be one of '+' or '-'.", ) @@ -700,7 +641,7 @@ class AbstractFieldProjectionMonitor(SurfaceIntegrationMonitor, FreqMonitor): custom_origin: Coordinate = pydantic.Field( None, - title="Local origin", + title="Local Origin", description="Local origin used for defining observation points. If ``None``, uses the " "monitor's center.", units=MICROMETER, @@ -708,7 +649,7 @@ class AbstractFieldProjectionMonitor(SurfaceIntegrationMonitor, FreqMonitor): far_field_approx: bool = pydantic.Field( True, - title="Far field approximation", + title="Far Field Approximation", description="Whether to enable the far field approximation when projecting fields. " "If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components " "of fields. Typically, this should be set to ``True`` only when the projection distance " @@ -716,6 +657,73 @@ class AbstractFieldProjectionMonitor(SurfaceIntegrationMonitor, FreqMonitor): "in the far field of the device.", ) + interval_space: Tuple[ + pydantic.PositiveInt, pydantic.PositiveInt, pydantic.PositiveInt + ] = pydantic.Field( + (1, 1, 1), + title="Spatial Interval", + description="Number of grid step intervals at which near fields are recorded for " + "projection to the far field, along each direction. If equal to 1, there will be no " + "downsampling. If greater than 1, the step will be applied, but the first and last " + "point of the monitor grid are always included. Using values greater than 1 can " + "help speed up server-side far field projections with minimal accuracy loss, " + "especially in cases where it is necessary for the grid resolution to be high for " + "the FDTD simulation, but such a high resolution is unnecessary for the purpose of " + "projecting the recorded near fields to the far field.", + ) + + window_size: Tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat] = pydantic.Field( + (0, 0), + title="Spatial filtering window size", + description="Size of the transition region of the windowing function used to ensure that " + "the recorded near fields decay to zero near the edges of the monitor. " + "The two components refer to the two tangential directions associated with each surface. " + "For surfaces with the normal along ``x``, the two components are (``y``, ``z``). " + "For surfaces with the normal along ``y``, the two components are (``x``, ``z``). " + "For surfaces with the normal along ``z``, the two components are (``x``, ``y``). " + "Each value must be between 0 and 1, inclusive, and denotes the size of the transition " + "region over which fields are scaled to less than a thousandth of the original amplitude, " + "relative to half the size of the monitor in that direction. A value of 0 turns windowing " + "off in that direction, while a value of 1 indicates that the window will be applied to " + "the entire monitor in that direction. This field is applicable for surface monitors only, " + "and otherwise must remain (0, 0).", + ) + + medium: MediumType = pydantic.Field( + None, + title="Projection medium", + description="Medium through which to project fields. Generally, the fields should be " + "projected through the same medium as the one in which this monitor is placed, and " + "this is the default behavior when ``medium=None``. A custom ``medium`` can be useful " + "in some situations for advanced users, but we recommend trying to avoid using a " + "non-default ``medium``.", + ) + + @pydantic.validator("window_size", always=True) + def window_size_for_surface(cls, val, values): + """Ensures that windowing is applied for surface monitors only.""" + size = values.get("size") + name = values.get("name") + + if size.count(0.0) != 1: + if val != (0, 0): + raise ValidationError( + f"A non-zero 'window_size' cannot be used for projection monitor '{name}'. " + "Windowing can be applied only for surface projection monitors." + ) + return val + + @pydantic.validator("window_size", always=True) + def window_size_leq_one(cls, val, values): + """Ensures that each component of the window size is less than or equal to 1.""" + name = values.get("name") + if val[0] > 1 or val[1] > 1: + raise ValidationError( + f"Each component of 'window_size' for monitor '{name}' " + "must be less than or equal to 1." + ) + return val + @property def projection_surfaces(self) -> Tuple[FieldProjectionSurface, ...]: """Surfaces of the monitor where near fields will be recorded for subsequent projection.""" @@ -741,6 +749,62 @@ def local_origin(self) -> Coordinate: return self.center return self.custom_origin + def window_parameters(self, custom_bounds: Bound = None) -> Tuple[Size, Coordinate, Coordinate]: + """Return the physical size of the window transition region based on the monitor's size + and optional custom bounds (useful in case the monitor has infinite dimensions). The window + size is returned in 3D. Also returns the coordinate where the transition region beings on + the minus and plus side of the monitor.""" + + window_size = [0, 0, 0] + window_minus = [0, 0, 0] + window_plus = [0, 0, 0] + + # windowing is for surface monitors only + if self.size.count(0.0) != 1: + return window_size, window_minus, window_plus + + _, plane_inds = self.pop_axis([0, 1, 2], axis=self.size.index(0.0)) + + for i, ind in enumerate(plane_inds): + if custom_bounds: + size = min(self.size[ind], custom_bounds[1][ind] - custom_bounds[0][ind]) + bound_min = max(self.bounds[0][ind], custom_bounds[0][ind]) + bound_max = min(self.bounds[1][ind], custom_bounds[1][ind]) + else: + size = self.size[ind] + bound_min = self.bounds[0][ind] + bound_max = self.bounds[1][ind] + + window_size[ind] = self.window_size[i] * size / 2 + window_minus[ind] = bound_min + window_size[ind] + window_plus[ind] = bound_max - window_size[ind] + + return window_size, window_minus, window_plus + + @staticmethod + def window_function( + points: ArrayFloat1D, + window_size: Size, + window_minus: Coordinate, + window_plus: Coordinate, + dim: int, + ) -> ArrayFloat1D: + """Get the windowing function along a given direction for a given set of points.""" + rising_window = np.exp( + -0.5 + * WINDOW_FACTOR + * ((points[points < window_minus[dim]] - window_minus[dim]) / window_size[dim]) ** 2 + ) + falling_window = np.exp( + -0.5 + * WINDOW_FACTOR + * ((points[points > window_plus[dim]] - window_plus[dim]) / window_size[dim]) ** 2 + ) + window_fn = np.ones_like(points) + window_fn[points < window_minus[dim]] = rising_window + window_fn[points > window_plus[dim]] = falling_window + return window_fn + class FieldProjectionAngleMonitor(AbstractFieldProjectionMonitor): """:class:`Monitor` that samples electromagnetic near fields in the frequency domain @@ -773,14 +837,14 @@ class FieldProjectionAngleMonitor(AbstractFieldProjectionMonitor): proj_distance: float = pydantic.Field( 1e6, - title="Projection distance", + title="Projection Distance", description="Radial distance of the projection points from ``local_origin``.", units=MICROMETER, ) theta: ObsGridArray = pydantic.Field( ..., - title="Polar angles", + title="Polar Angles", description="Polar angles with respect to the global z axis, relative to the location of " "``local_origin``, at which to project fields.", units=RADIAN, @@ -788,7 +852,7 @@ class FieldProjectionAngleMonitor(AbstractFieldProjectionMonitor): phi: ObsGridArray = pydantic.Field( ..., - title="Azimuth angles", + title="Azimuth Angles", description="Azimuth angles with respect to the global z axis, relative to the location of " "``local_origin``, at which to project fields.", units=RADIAN, @@ -836,13 +900,13 @@ class FieldProjectionCartesianMonitor(AbstractFieldProjectionMonitor): proj_axis: Axis = pydantic.Field( ..., - title="Projection plane axis", + title="Projection Plane Axis", description="Axis along which the observation plane is oriented.", ) proj_distance: float = pydantic.Field( 1e6, - title="Projection distance", + title="Projection Distance", description="Signed distance of the projection plane along ``proj_axis``. " "from the plane containing ``local_origin``.", units=MICROMETER, @@ -850,7 +914,7 @@ class FieldProjectionCartesianMonitor(AbstractFieldProjectionMonitor): x: ObsGridArray = pydantic.Field( ..., - title="Local x observation coordinates", + title="Local x Observation Coordinates", description="Local x observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. " "When ``proj_axis`` is 0, this corresponds to the global y axis. " "When ``proj_axis`` is 1, this corresponds to the global x axis. " @@ -860,7 +924,7 @@ class FieldProjectionCartesianMonitor(AbstractFieldProjectionMonitor): y: ObsGridArray = pydantic.Field( ..., - title="Local y observation coordinates", + title="Local y Observation Coordinates", description="Local y observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. " "When ``proj_axis`` is 0, this corresponds to the global z axis. " "When ``proj_axis`` is 1, this corresponds to the global z axis. " @@ -909,13 +973,13 @@ class FieldProjectionKSpaceMonitor(AbstractFieldProjectionMonitor): proj_axis: Axis = pydantic.Field( ..., - title="Projection plane axis", + title="Projection Plane Axis", description="Axis along which the observation plane is oriented.", ) proj_distance: float = pydantic.Field( 1e6, - title="Projection distance", + title="Projection Distance", description="Radial distance of the projection points from ``local_origin``.", units=MICROMETER, ) @@ -974,7 +1038,7 @@ class DiffractionMonitor(PlanarMonitor, FreqMonitor): normal_dir: Direction = pydantic.Field( "+", - title="Normal vector orientation", + title="Normal Vector Orientation", description="Direction of the surface monitor's normal vector w.r.t. " "the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. " "Defaults to ``'+'`` if not provided.", @@ -982,7 +1046,7 @@ class DiffractionMonitor(PlanarMonitor, FreqMonitor): colocate: Literal[False] = pydantic.Field( False, - title="Colocate fields", + title="Colocate Fields", description="Defines whether fields are colocated to grid cell boundaries (i.e. to the " "primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors " "and is hard-coded for other monitors depending on their specific function.", diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py new file mode 100644 index 000000000..55a2efec6 --- /dev/null +++ b/tidy3d/components/scene.py @@ -0,0 +1,1342 @@ +""" Container holding about the geometry and medium properties common to all types of simulations. +""" +from __future__ import annotations +from typing import Dict, Tuple, List, Set, Union + +import pydantic.v1 as pd +import numpy as np +import matplotlib.pylab as plt +import matplotlib as mpl +from mpl_toolkits.axes_grid1 import make_axes_locatable + +from .base import cached_property, Tidy3dBaseModel +from .validators import assert_unique_names +from .geometry.base import Box, GeometryGroup, ClipOperation +from .geometry.utils import flatten_groups, traverse_geometries +from .types import Ax, Shapely, TYPE_TAG_STR, Bound, Size, Coordinate, InterpMethod +from .medium import Medium, MediumType +from .medium import AbstractCustomMedium, Medium2D, MediumType3D +from .medium import AbstractPerturbationMedium +from .grid.grid import Grid +from .structure import Structure +from .data.data_array import SpatialDataArray +from .viz import add_ax_if_none, equal_aspect +from .grid.grid import Coords +from .heat_spec import SolidSpec + +from .viz import MEDIUM_CMAP, STRUCTURE_EPS_CMAP, PlotParams, polygon_path, STRUCTURE_HEAT_COND_CMAP +from .viz import plot_params_structure, plot_params_fluid + +from ..constants import inf, THERMAL_CONDUCTIVITY +from ..exceptions import SetupError, Tidy3dError +from ..log import log + +# maximum number of mediums supported +MAX_NUM_MEDIUMS = 65530 + +# maximum geometry count in a single structure +MAX_GEOMETRY_COUNT = 100 + + +class Scene(Tidy3dBaseModel): + """Contains generic information about the geometry and medium properties common to all types of + simulations. + + Example + ------- + >>> sim = Scene( + ... structures=[ + ... Structure( + ... geometry=Box(size=(1, 1, 1), center=(0, 0, 0)), + ... medium=Medium(permittivity=2.0), + ... ), + ... ], + ... medium=Medium(permittivity=3.0), + ... ) + """ + + medium: MediumType3D = pd.Field( + Medium(), + title="Background Medium", + description="Background medium of scene, defaults to vacuum if not specified.", + discriminator=TYPE_TAG_STR, + ) + + structures: Tuple[Structure, ...] = pd.Field( + (), + title="Structures", + description="Tuple of structures present in scene. " + "Note: Structures defined later in this list override the " + "simulation material properties in regions of spatial overlap.", + ) + + """ Validating setup """ + + # make sure all names are unique + _unique_structure_names = assert_unique_names("structures") + + @pd.validator("structures", always=True) + def _validate_num_mediums(cls, val): + """Error if too many mediums present.""" + + if val is None: + return val + + mediums = {structure.medium for structure in val} + if len(mediums) > MAX_NUM_MEDIUMS: + raise SetupError( + f"Tidy3D only supports {MAX_NUM_MEDIUMS} distinct mediums." + f"{len(mediums)} were supplied." + ) + + return val + + @pd.validator("structures", always=True) + def _validate_num_geometries(cls, val): + """Error if too many geometries in a single structure.""" + + if val is None: + return val + + for i, structure in enumerate(val): + for geometry in flatten_groups(structure.geometry): + count = sum( + 1 + for g in traverse_geometries(geometry) + if not isinstance(g, (GeometryGroup, ClipOperation)) + ) + if count > MAX_GEOMETRY_COUNT: + raise SetupError( + f"Structure at 'structures[{i}]' has {count} geometries that cannot be " + f"flattened. A maximum of {MAX_GEOMETRY_COUNT} is supported due to " + f"preprocessing performance." + ) + + return val + + """ Accounting """ + + @cached_property + def bounds(self) -> Bound: + """Automatically defined scene's bounds based on present structures. Infinite dimensions + are ignored. If the scene contains no strucutres, the bounds are set to + (-1, -1, -1), (1, 1, 1). Similarly, if along a given axis all structures extend infinitely, + the bounds along that axis are set from -1 to 1. + + Returns + ------- + Tuple[float, float, float], Tuple[float, float, float] + Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. + """ + + bounds = tuple(structure.geometry.bounds for structure in self.structures) + return ( + tuple(min((b[i] for b, _ in bounds if b[i] != -inf), default=-1) for i in range(3)), + tuple(max((b[i] for _, b in bounds if b[i] != inf), default=1) for i in range(3)), + ) + + @cached_property + def size(self) -> Size: + """Automatically defined scene's size. + + Returns + ------- + Tuple[float, float, float] + Scene's size. + """ + + return tuple(bmax - bmin for bmin, bmax in zip(self.bounds[0], self.bounds[1])) + + @cached_property + def center(self) -> Coordinate: + """Automatically defined scene's center. + + Returns + ------- + Tuple[float, float, float] + Scene's center. + """ + + return tuple(0.5 * (bmin + bmax) for bmin, bmax in zip(self.bounds[0], self.bounds[1])) + + @cached_property + def box(self) -> Box: + """Automatically defined scene's :class:`.Box`. + + Returns + ------- + Box + Scene's box. + """ + + return Box(center=self.center, size=self.size) + + @cached_property + def mediums(self) -> Set[MediumType]: + """Returns set of distinct :class:`.AbstractMedium` in scene. + + Returns + ------- + List[:class:`.AbstractMedium`] + Set of distinct mediums in the scene. + """ + medium_dict = {self.medium: None} + medium_dict.update({structure.medium: None for structure in self.structures}) + return list(medium_dict.keys()) + + @cached_property + def medium_map(self) -> Dict[MediumType, pd.NonNegativeInt]: + """Returns dict mapping medium to index in material. + ``medium_map[medium]`` returns unique global index of :class:`.AbstractMedium` in scene. + + Returns + ------- + Dict[:class:`.AbstractMedium`, int] + Mapping between distinct mediums to index in scene. + """ + + return {medium: index for index, medium in enumerate(self.mediums)} + + @cached_property + def background_structure(self) -> Structure: + """Returns structure representing the background of the :class:`.Scene`.""" + geometry = Box(size=(inf, inf, inf)) + return Structure(geometry=geometry, medium=self.medium) + + @staticmethod + def intersecting_media( + test_object: Box, structures: Tuple[Structure, ...] + ) -> Tuple[MediumType, ...]: + """From a given list of structures, returns a list of :class:`.AbstractMedium` associated + with those structures that intersect with the ``test_object``, if it is a surface, or its + surfaces, if it is a volume. + + Parameters + ------- + test_object : :class:`.Box` + Object for which intersecting media are to be detected. + structures : List[:class:`.AbstractMedium`] + List of structures whose media will be tested. + + Returns + ------- + List[:class:`.AbstractMedium`] + Set of distinct mediums that intersect with the given planar object. + """ + if test_object.size.count(0.0) == 1: + # get all merged structures on the test_object, which is already planar + structures_merged = Scene._filter_structures_plane_medium(structures, test_object) + mediums = {medium for medium, _ in structures_merged} + return mediums + + # if the test object is a volume, test each surface recursively + surfaces = test_object.surfaces_with_exclusion(**test_object.dict()) + mediums = set() + for surface in surfaces: + _mediums = Scene.intersecting_media(surface, structures) + mediums.update(_mediums) + return mediums + + @staticmethod + def intersecting_structures( + test_object: Box, structures: Tuple[Structure, ...] + ) -> Tuple[Structure, ...]: + """From a given list of structures, returns a list of :class:`.Structure` that intersect + with the ``test_object``, if it is a surface, or its surfaces, if it is a volume. + + Parameters + ------- + test_object : :class:`.Box` + Object for which intersecting media are to be detected. + structures : List[:class:`.AbstractMedium`] + List of structures whose media will be tested. + + Returns + ------- + List[:class:`.Structure`] + Set of distinct structures that intersect with the given surface, or with the surfaces + of the given volume. + """ + if test_object.size.count(0.0) == 1: + # get all merged structures on the test_object, which is already planar + normal_axis_index = test_object.size.index(0.0) + dim = "xyz"[normal_axis_index] + pos = test_object.center[normal_axis_index] + xyz_kwargs = {dim: pos} + + structures_merged = [] + for structure in structures: + intersections = structure.geometry.intersections_plane(**xyz_kwargs) + if len(intersections) > 0: + structures_merged.append(structure) + return structures_merged + + # if the test object is a volume, test each surface recursively + surfaces = test_object.surfaces_with_exclusion(**test_object.dict()) + structures_merged = [] + for surface in surfaces: + structures_merged += Scene.intersecting_structures(surface, structures) + return structures_merged + + """ Plotting General """ + + @staticmethod + def _get_plot_lims( + bounds: Bound, + x: float = None, + y: float = None, + z: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Tuple[Tuple[float, float], Tuple[float, float]]: + + # if no hlim and/or vlim given, the bounds will then be the usual pml bounds + axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) + _, (hmin, vmin) = Box.pop_axis(bounds[0], axis=axis) + _, (hmax, vmax) = Box.pop_axis(bounds[1], axis=axis) + + # account for unordered limits + if hlim is None: + hlim = (hmin, hmax) + if vlim is None: + vlim = (vmin, vmax) + + if hlim[0] > hlim[1]: + raise Tidy3dError("Error: 'hmin' > 'hmax'") + if vlim[0] > vlim[1]: + raise Tidy3dError("Error: 'vmin' > 'vmax'") + + return hlim, vlim + + @equal_aspect + @add_ax_if_none + def plot( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + **patch_kwargs, + ) -> Ax: + """Plot each of scene's components 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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + + ax = self.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + return ax + + @equal_aspect + @add_ax_if_none + def plot_structures( + self, + x: float = None, + y: float = None, + z: float = None, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """Plot each of scene'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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + medium_shapes = self._get_structures_2dbox( + structures=self.structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + medium_map = self.medium_map + for (medium, shape) in medium_shapes: + mat_index = medium_map[medium] + ax = self._plot_shape_structure(medium=medium, mat_index=mat_index, shape=shape, ax=ax) + ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + + # clean up the axis display + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + ax = self.box.add_ax_labels_lims(axis=axis, ax=ax) + ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") + + return ax + + def _plot_shape_structure(self, medium: Medium, mat_index: int, shape: Shapely, ax: Ax) -> Ax: + """Plot a structure's cross section shape for a given medium.""" + plot_params_struct = self._get_structure_plot_params(medium=medium, mat_index=mat_index) + ax = self.box.plot_shape(shape=shape, plot_params=plot_params_struct, ax=ax) + return ax + + def _get_structure_plot_params(self, mat_index: int, medium: Medium) -> PlotParams: + """Constructs the plot parameters for a given medium in scene.plot().""" + + plot_params = plot_params_structure.copy(update={"linewidth": 0}) + + if mat_index == 0 or medium == self.medium: + # background medium + plot_params = plot_params.copy(update={"facecolor": "white", "edgecolor": "white"}) + elif medium.is_pec: + # perfect electrical conductor + plot_params = plot_params.copy( + update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1} + ) + elif medium.time_modulated: + # time modulated medium + plot_params = plot_params.copy( + update={"facecolor": "red", "linewidth": 0, "hatch": "x*"} + ) + elif isinstance(medium, Medium2D): + # 2d material + plot_params = plot_params.copy(update={"edgecolor": "k", "linewidth": 1}) + else: + # regular medium + facecolor = MEDIUM_CMAP[(mat_index - 1) % len(MEDIUM_CMAP)] + plot_params = plot_params.copy(update={"facecolor": facecolor}) + + return plot_params + + @staticmethod + def _add_cbar(vmin: float, vmax: float, label: str, cmap: str, ax: Ax = None) -> None: + """Add a colorbar to plot.""" + norm = mpl.colors.Normalize(vmin=vmin, vmax=vmax) + divider = make_axes_locatable(ax) + cax = divider.append_axes("right", size="5%", pad=0.15) + mappable = mpl.cm.ScalarMappable(norm=norm, cmap=cmap) + plt.colorbar(mappable, cax=cax, label=label) + + @staticmethod + def _set_plot_bounds( + bounds: Bound, + ax: Ax, + x: float = None, + y: float = None, + z: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """Sets the xy limits of the scene 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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + Returns + ------- + matplotlib.axes._subplots.Axes + The axes after setting the boundaries. + """ + + hlim, vlim = Scene._get_plot_lims(bounds=bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax.set_xlim(hlim) + ax.set_ylim(vlim) + return ax + + def _get_structures_2dbox( + self, + structures: List[Structure], + x: float = None, + y: float = None, + z: float = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> List[Tuple[Medium, Shapely]]: + """Compute list of shapes to plot on 2d box specified by (x_min, x_max), (y_min, y_max). + + Parameters + ---------- + structures : List[:class:`.Structure`] + list of structures to filter on the plane. + 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. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + List[Tuple[:class:`.AbstractMedium`, shapely.geometry.base.BaseGeometry]] + List of shapes and mediums on the plane. + """ + # if no hlim and/or vlim given, the bounds will then be the usual pml bounds + axis, _ = Box.parse_xyz_kwargs(x=x, y=y, z=z) + _, (hmin, vmin) = Box.pop_axis(self.bounds[0], axis=axis) + _, (hmax, vmax) = Box.pop_axis(self.bounds[1], axis=axis) + + if hlim is not None: + (hmin, hmax) = hlim + if vlim is not None: + (vmin, vmax) = vlim + + # get center and size with h, v + h_center = (hmin + hmax) / 2.0 + v_center = (vmin + vmax) / 2.0 + h_size = (hmax - hmin) or inf + v_size = (vmax - vmin) or inf + + axis, center_normal = Box.parse_xyz_kwargs(x=x, y=y, z=z) + center = Box.unpop_axis(center_normal, (h_center, v_center), axis=axis) + size = Box.unpop_axis(0.0, (h_size, v_size), axis=axis) + plane = Box(center=center, size=size) + + medium_shapes = [] + for structure in structures: + intersections = plane.intersections_with(structure.geometry) + for shape in intersections: + if not shape.is_empty: + shape = Box.evaluate_inf_shape(shape) + medium_shapes.append((structure.medium, shape)) + return medium_shapes + + @staticmethod + def _filter_structures_plane_medium( + structures: List[Structure], plane: Box + ) -> List[Tuple[Medium, Shapely]]: + """Compute list of shapes to plot on plane. Overlaps are removed or merged depending on + medium. + + Parameters + ---------- + structures : List[:class:`.Structure`] + List of structures to filter on the plane. + plane : Box + Plane specification. + + Returns + ------- + List[Tuple[:class:`.AbstractMedium`, shapely.geometry.base.BaseGeometry]] + List of shapes and mediums on the plane after merging. + """ + + medium_list = [structure.medium for structure in structures] + return Scene._filter_structures_plane( + structures=structures, plane=plane, property_list=medium_list + ) + + @staticmethod + def _filter_structures_plane( + structures: List[Structure], + plane: Box, + property_list: List, + ) -> List[Tuple[Medium, Shapely]]: + """Compute list of shapes to plot on plane. Overlaps are removed or merged depending on + provided property_list. + + Parameters + ---------- + structures : List[:class:`.Structure`] + List of structures to filter on the plane. + plane : Box + Plane specification. + property_list : List = None + Property value for each structure. + + Returns + ------- + List[Tuple[:class:`.AbstractMedium`, shapely.geometry.base.BaseGeometry]] + List of shapes and their property value on the plane after merging. + """ + + if len(structures) != len(property_list): + raise SetupError( + "Number of provided property values is not equal to the number of structures." + ) + + shapes = [] + for structure, prop in zip(structures, property_list): + + # get list of Shapely shapes that intersect at the plane + shapes_plane = plane.intersections_with(structure.geometry) + + # Append each of them and their property information to the list of shapes + for shape in shapes_plane: + shapes.append((prop, shape, shape.bounds)) + + background_shapes = [] + for prop, shape, bounds in shapes: + + minx, miny, maxx, maxy = bounds + + # loop through background_shapes (note: all background are non-intersecting or merged) + for index, (_prop, _shape, _bounds) in enumerate(background_shapes): + + _minx, _miny, _maxx, _maxy = _bounds + + # do a bounding box check to see if any intersection to do anything about + if minx > _maxx or _minx > maxx or miny > _maxy or _miny > maxy: + continue + + # look more closely to see if intersected. + if _shape.is_empty or not shape.intersects(_shape): + continue + + diff_shape = _shape - shape + + # different prop, remove intersection from background shape + if prop != _prop and len(diff_shape.bounds) > 0: + background_shapes[index] = (_prop, diff_shape, diff_shape.bounds) + + # same prop, add diff shape to this shape and mark background shape for removal + else: + shape = shape | diff_shape + background_shapes[index] = None + + # after doing this with all background shapes, add this shape to the background + background_shapes.append((prop, shape, shape.bounds)) + + # remove any existing background shapes that have been marked as 'None' + background_shapes = [b for b in background_shapes if b is not None] + + # filter out any remaining None or empty shapes (shapes with area completely removed) + return [(prop, shape) for (prop, shape, _) in background_shapes if shape] + + """ Plotting Optical """ + + @equal_aspect + @add_ax_if_none + def plot_eps( + self, + x: float = None, + y: float = None, + z: float = None, + freq: float = None, + alpha: float = None, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """Plot each of scene'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. + alpha : float = None + Opacity of the structures being plotted. + Defaults to the structure default alpha. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + + ax = self.plot_structures_eps( + freq=freq, cbar=True, alpha=alpha, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + return ax + + @equal_aspect + @add_ax_if_none + def plot_structures_eps( + self, + x: float = None, + y: float = None, + z: float = None, + freq: float = None, + alpha: float = None, + cbar: bool = True, + reverse: bool = False, + eps_lim: Tuple[Union[float, None], Union[float, None]] = (None, None), + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + grid: Grid = None, + ) -> Ax: + """Plot each of scene'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. + reverse : bool = False + If ``False``, the highest permittivity is plotted in black. + If ``True``, it is plotteed in white (suitable for black backgrounds). + cbar : bool = True + Whether to plot a colorbar for the relative permittivity. + alpha : float = None + Opacity of the structures being plotted. + Defaults to the structure default alpha. + eps_lim : Tuple[float, float] = None + Custom limits for eps coloring. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + structures = self.structures + + # alpha is None just means plot without any transparency + if alpha is None: + alpha = 1 + + if alpha <= 0: + return ax + + if alpha < 1 and not isinstance(self.medium, AbstractCustomMedium): + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + center = Box.unpop_axis(position, (0, 0), axis=axis) + size = Box.unpop_axis(0, (inf, inf), axis=axis) + plane = Box(center=center, size=size) + medium_shapes = self._filter_structures_plane_medium(structures=structures, plane=plane) + else: + structures = [self.background_structure] + list(structures) + medium_shapes = self._get_structures_2dbox( + structures=structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + + eps_min, eps_max = eps_lim + + if eps_min is None or eps_max is None: + + eps_min_sim, eps_max_sim = self.eps_bounds(freq=freq) + + if eps_min is None: + eps_min = eps_min_sim + + if eps_max is None: + eps_max = eps_max_sim + + for (medium, shape) in medium_shapes: + # if the background medium is custom medium, it needs to be rendered separately + if medium == self.medium and alpha < 1 and not isinstance(medium, AbstractCustomMedium): + continue + # no need to add patches for custom medium + if not isinstance(medium, AbstractCustomMedium): + ax = self._plot_shape_structure_eps( + freq=freq, + alpha=alpha, + medium=medium, + eps_min=eps_min, + eps_max=eps_max, + reverse=reverse, + shape=shape, + ax=ax, + ) + else: + # For custom medium, apply pcolormesh clipped by the shape. + self._pcolormesh_shape_custom_medium_structure_eps( + x, y, z, freq, alpha, medium, eps_min, eps_max, reverse, shape, ax, grid + ) + + if cbar: + self._add_cbar_eps(eps_min=eps_min, eps_max=eps_max, ax=ax) + ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + + # clean up the axis display + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + ax = self.box.add_ax_labels_lims(axis=axis, ax=ax) + ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") + + return ax + + @staticmethod + def _add_cbar_eps(eps_min: float, eps_max: float, ax: Ax = None) -> None: + """Add a permittivity colorbar to plot.""" + Scene._add_cbar( + vmin=eps_min, vmax=eps_max, label=r"$\epsilon_r$", cmap=STRUCTURE_EPS_CMAP, ax=ax + ) + + def eps_bounds(self, freq: float = None) -> Tuple[float, float]: + """Compute range of (real) permittivity present in the scene at frequency "freq". + + Parameters + ---------- + freq : float = None + Frequency to evaluate the relative permittivity of all mediums. + If not specified, evaluates at infinite frequency. + + Returns + ------- + Tuple[float, float] + Minimal and maximal values of relative permittivity in scene. + """ + + medium_list = [self.medium] + list(self.mediums) + medium_list = [medium for medium in medium_list if not medium.is_pec] + # regular medium + eps_list = [ + medium.eps_model(freq).real + for medium in medium_list + if not isinstance(medium, AbstractCustomMedium) and not isinstance(medium, Medium2D) + ] + eps_min = min(eps_list, default=1) + eps_max = max(eps_list, default=1) + # custom medium, the min and max in the supplied dataset over all components and + # spatial locations. + for mat in [medium for medium in medium_list if isinstance(medium, AbstractCustomMedium)]: + eps_dataarray = mat.eps_dataarray_freq(freq) + eps_min = min( + eps_min, + min(np.min(eps_comp.real.values.ravel()) for eps_comp in eps_dataarray), + ) + eps_max = max( + eps_max, + max(np.max(eps_comp.real.values.ravel()) for eps_comp in eps_dataarray), + ) + return eps_min, eps_max + + def _pcolormesh_shape_custom_medium_structure_eps( + self, + x: float, + y: float, + z: float, + freq: float, + alpha: float, + medium: Medium, + eps_min: float, + eps_max: float, + reverse: bool, + shape: Shapely, + ax: Ax, + grid: Grid, + ): + """ + Plot shape made of custom medium with ``pcolormesh``. + """ + coords = "xyz" + normal_axis_ind, normal_position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + normal_axis, plane_axes = Box.pop_axis(coords, normal_axis_ind) + + # make grid for eps interpolation + # we will do this by combining shape bounds and points where custom eps is provided + shape_bounds = shape.bounds + rmin, rmax = [*shape_bounds[:2]], [*shape_bounds[2:]] + rmin.insert(normal_axis_ind, normal_position) + rmax.insert(normal_axis_ind, normal_position) + + if grid is None: + plane_axes_inds = [0, 1, 2] + plane_axes_inds.pop(normal_axis_ind) + + # in case when different components of custom medium are defined on different grids + # we will combine all points along each dimension + eps_diag = medium.eps_dataarray_freq(frequency=freq) + if ( + eps_diag[0].coords == eps_diag[1].coords + and eps_diag[0].coords == eps_diag[2].coords + ): + coords_to_insert = [eps_diag[0].coords] + else: + coords_to_insert = [eps_diag[0].coords, eps_diag[1].coords, eps_diag[2].coords] + + # actual combining of points along each of plane dimensions + plane_coord = [] + for ind, comp in zip(plane_axes_inds, plane_axes): + # first start with an array made of shapes bounds + axis_coords = np.array([rmin[ind], rmax[ind]]) + # now add points in between them + for coords in coords_to_insert: + comp_axis_coords = coords[comp] + inds_inside_shape = np.where( + np.logical_and(comp_axis_coords > rmin[ind], comp_axis_coords < rmax[ind]) + )[0] + if len(inds_inside_shape) > 0: + axis_coords = np.concatenate( + (axis_coords, comp_axis_coords[inds_inside_shape]) + ) + # remove duplicates + axis_coords = np.unique(axis_coords) + + plane_coord.append(axis_coords) + else: + span_inds = grid.discretize_inds(Box.from_bounds(rmin=rmin, rmax=rmax), extend=True) + # filter negative or too large inds + n_grid = [len(grid_comp) for grid_comp in grid.boundaries.to_list] + span_inds = [ + (max(fmin, 0), min(fmax, n_grid[f_ind])) + for f_ind, (fmin, fmax) in enumerate(span_inds) + ] + + # assemble the coordinate in the 2d plane + plane_coord = [] + for plane_axis in range(2): + ind_axis = "xyz".index(plane_axes[plane_axis]) + plane_coord.append(grid.boundaries.to_list[ind_axis][slice(*span_inds[ind_axis])]) + + # prepare `Coords` for interpolation + coord_dict = { + plane_axes[0]: plane_coord[0], + plane_axes[1]: plane_coord[1], + normal_axis: [normal_position], + } + coord_shape = Coords(**coord_dict) + # interpolate permittivity and take the average over components + eps_shape = np.mean(medium.eps_diagonal_on_grid(frequency=freq, coords=coord_shape), axis=0) + # remove the normal_axis and take real part + eps_shape = eps_shape.real.mean(axis=normal_axis_ind) + # reverse + if reverse: + eps_shape = eps_min + eps_max - eps_shape + + # pcolormesh + plane_xp, plane_yp = np.meshgrid(plane_coord[0], plane_coord[1], indexing="ij") + ax.pcolormesh( + plane_xp, + plane_yp, + eps_shape, + clip_path=(polygon_path(shape), ax.transData), + cmap=STRUCTURE_EPS_CMAP, + vmin=eps_min, + vmax=eps_max, + alpha=alpha, + clip_box=ax.bbox, + ) + + def _get_structure_eps_plot_params( + self, + medium: Medium, + freq: float, + eps_min: float, + eps_max: float, + reverse: bool = False, + alpha: float = None, + ) -> PlotParams: + """Constructs the plot parameters for a given medium in scene.plot_eps().""" + + plot_params = plot_params_structure.copy(update={"linewidth": 0}) + if alpha is not None: + plot_params = plot_params.copy(update={"alpha": alpha}) + + if medium.is_pec: + # perfect electrical conductor + plot_params = plot_params.copy( + update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1} + ) + elif isinstance(medium, Medium2D): + # 2d material + plot_params = plot_params.copy(update={"edgecolor": "k", "linewidth": 1}) + else: + # regular medium + eps_medium = medium.eps_model(frequency=freq).real + delta_eps = eps_medium - eps_min + delta_eps_max = eps_max - eps_min + 1e-5 + eps_fraction = delta_eps / delta_eps_max + color = eps_fraction if reverse else 1 - eps_fraction + color = min(1, max(color, 0)) # clip in case of custom eps limits + plot_params = plot_params.copy(update={"facecolor": str(color)}) + + return plot_params + + def _plot_shape_structure_eps( + self, + freq: float, + medium: Medium, + shape: Shapely, + eps_min: float, + eps_max: float, + ax: Ax, + reverse: bool = False, + alpha: float = None, + ) -> Ax: + """Plot a structure's cross section shape for a given medium, grayscale for permittivity.""" + plot_params = self._get_structure_eps_plot_params( + medium=medium, freq=freq, eps_min=eps_min, eps_max=eps_max, alpha=alpha, reverse=reverse + ) + ax = self.box.plot_shape(shape=shape, plot_params=plot_params, ax=ax) + return ax + + """ Plotting Heat """ + + @equal_aspect + @add_ax_if_none + def plot_heat_conductivity( + self, + x: float = None, + y: float = None, + z: float = None, + alpha: float = None, + cbar: bool = True, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """Plot each of scebe's components on a plane defined by one nonzero x,y,z coordinate. + The thermal conductivity is plotted in grayscale based on its value. + + 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. + alpha : float = None + Opacity of the structures being plotted. + Defaults to the structure default alpha. + cbar : bool = True + Whether to plot a colorbar for the thermal conductivity. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + hlim, vlim = Scene._get_plot_lims(bounds=self.bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + + ax = self.plot_structures_heat_conductivity( + cbar=cbar, alpha=alpha, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + return ax + + @equal_aspect + @add_ax_if_none + def plot_structures_heat_conductivity( + self, + x: float = None, + y: float = None, + z: float = None, + alpha: float = None, + cbar: bool = True, + reverse: bool = False, + ax: Ax = None, + hlim: Tuple[float, float] = None, + vlim: Tuple[float, float] = None, + ) -> Ax: + """Plot each of scene's structures on a plane defined by one nonzero x,y,z coordinate. + The thermal conductivity is plotted in grayscale based on its value. + + 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. + reverse : bool = False + If ``False``, the highest permittivity is plotted in black. + If ``True``, it is plotteed in white (suitable for black backgrounds). + cbar : bool = True + Whether to plot a colorbar for the relative permittivity. + alpha : float = None + Opacity of the structures being plotted. + Defaults to the structure default alpha. + ax : matplotlib.axes._subplots.Axes = None + Matplotlib axes to plot on, if not specified, one is created. + hlim : Tuple[float, float] = None + The x range if plotting on xy or xz planes, y range if plotting on yz plane. + vlim : Tuple[float, float] = None + The z range if plotting on xz or yz planes, y plane if plotting on xy plane. + + Returns + ------- + matplotlib.axes._subplots.Axes + The supplied or created matplotlib axes. + """ + + structures = self.structures + + # alpha is None just means plot without any transparency + if alpha is None: + alpha = 1 + + if alpha <= 0: + return ax + + if alpha < 1: + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + center = Box.unpop_axis(position, (0, 0), axis=axis) + size = Box.unpop_axis(0, (inf, inf), axis=axis) + plane = Box(center=center, size=size) + medium_shapes = self._filter_structures_plane_medium(structures=structures, plane=plane) + else: + structures = [self.background_structure] + list(structures) + medium_shapes = self._get_structures_2dbox( + structures=structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) + + heat_cond_min, heat_cond_max = self.heat_conductivity_bounds() + for (medium, shape) in medium_shapes: + ax = self._plot_shape_structure_heat_cond( + alpha=alpha, + medium=medium, + heat_cond_min=heat_cond_min, + heat_cond_max=heat_cond_max, + reverse=reverse, + shape=shape, + ax=ax, + ) + + if cbar: + self._add_cbar( + vmin=heat_cond_min, + vmax=heat_cond_max, + label=f"Thermal conductivity ({THERMAL_CONDUCTIVITY})", + cmap=STRUCTURE_HEAT_COND_CMAP, + ax=ax, + ) + ax = self._set_plot_bounds(bounds=self.bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + + # clean up the axis display + axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) + ax = self.box.add_ax_labels_lims(axis=axis, ax=ax) + ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") + + return ax + + def heat_conductivity_bounds(self) -> Tuple[float, float]: + """Compute range of thermal conductivities present in the scene. + + Returns + ------- + Tuple[float, float] + Minimal and maximal values of thermal conductivity in scene. + """ + + medium_list = [self.medium] + list(self.mediums) + medium_list = [medium for medium in medium_list if isinstance(medium.heat_spec, SolidSpec)] + cond_list = [medium.heat_spec.conductivity for medium in medium_list] + cond_min = min(cond_list) + cond_max = max(cond_list) + return cond_min, cond_max + + def _get_structure_heat_cond_plot_params( + self, + medium: Medium, + heat_cond_min: float, + heat_cond_max: float, + reverse: bool = False, + alpha: float = None, + ) -> PlotParams: + """Constructs the plot parameters for a given medium in + scene.plot_heat_conductivity(). + """ + + plot_params = plot_params_structure.copy(update={"linewidth": 0}) + if alpha is not None: + plot_params = plot_params.copy(update={"alpha": alpha}) + + if isinstance(medium.heat_spec, SolidSpec): + # regular medium + cond_medium = medium.heat_spec.conductivity + delta_cond = cond_medium - heat_cond_min + delta_cond_max = heat_cond_max - heat_cond_min + 1e-5 * heat_cond_min + cond_fraction = delta_cond / delta_cond_max + color = cond_fraction if reverse else 1 - cond_fraction + plot_params = plot_params.copy(update={"facecolor": str(color)}) + else: + plot_params = plot_params_fluid + if alpha is not None: + plot_params = plot_params.copy(update={"alpha": alpha}) + + return plot_params + + def _plot_shape_structure_heat_cond( + self, + medium: Medium, + shape: Shapely, + heat_cond_min: float, + heat_cond_max: float, + ax: Ax, + reverse: bool = False, + alpha: float = None, + ) -> Ax: + """Plot a structure's cross section shape for a given medium, grayscale for thermal + conductivity. + """ + plot_params = self._get_structure_heat_cond_plot_params( + medium=medium, + heat_cond_min=heat_cond_min, + heat_cond_max=heat_cond_max, + alpha=alpha, + reverse=reverse, + ) + ax = self.box.plot_shape(shape=shape, plot_params=plot_params, ax=ax) + return ax + + """ Misc """ + + def perturbed_mediums_copy( + self, + temperature: SpatialDataArray = None, + electron_density: SpatialDataArray = None, + hole_density: SpatialDataArray = None, + interp_method: InterpMethod = "linear", + ) -> Scene: + """Return a copy of the scene with heat and/or charge data applied to all mediums + that have perturbation models specified. That is, such mediums will be replaced with + spatially dependent custom mediums that reflect perturbation effects. Any of temperature, + electron_density, and hole_density can be ``None``. All provided fields must have identical + coords. + + Parameters + ---------- + temperature : SpatialDataArray = None + Temperature field data. + electron_density : SpatialDataArray = None + Electron density field data. + hole_density : SpatialDataArray = None + Hole density field data. + interp_method : :class:`.InterpMethod`, optional + Interpolation method to obtain heat and/or charge values that are not supplied + at the Yee grids. + + Returns + ------- + Scene + Simulation after application of heat and/or charge data. + """ + + scene_dict = self.dict() + structures = self.structures + scene_bounds = self.bounds + array_dict = { + "temperature": temperature, + "electron_density": electron_density, + "hole_density": hole_density, + } + + # For each structure made of mediums with perturbation models, convert those mediums into + # spatially dependent mediums by selecting minimal amount of heat and charge data points + # covering the structure, and create a new structure containing the resulting custom medium + new_structures = [] + for s_ind, structure in enumerate(structures): + med = structure.medium + if isinstance(med, AbstractPerturbationMedium): + # get structure's bounding box + s_bounds = structure.geometry.bounds + + bounds = [ + np.max([scene_bounds[0], s_bounds[0]], axis=0), + np.min([scene_bounds[1], s_bounds[1]], axis=0), + ] + + # for each structure select a minimal subset of data that covers it + restricted_arrays = {} + + for name, array in array_dict.items(): + if array is not None: + restricted_arrays[name] = array.sel_inside(bounds) + + # check provided data fully cover structure + if not array.does_cover(bounds): + log.warning( + f"Provided '{name}' does not fully cover structures[{s_ind}]." + ) + + new_medium = med.perturbed_copy(**restricted_arrays, interp_method=interp_method) + new_structure = structure.updated_copy(medium=new_medium) + new_structures.append(new_structure) + else: + new_structures.append(structure) + + scene_dict["structures"] = new_structures + + # do the same for background medium if it a medium with perturbation models. + med = self.medium + if isinstance(med, AbstractPerturbationMedium): + + # get scene's bounding box + bounds = scene_bounds + + # for each structure select a minimal subset of data that covers it + restricted_arrays = {} + + for name, array in array_dict.items(): + if array is not None: + restricted_arrays[name] = array.sel_inside(bounds) + + # check provided data fully cover scene + if not array.does_cover(bounds): + log.warning(f"Provided '{name}' does not fully cover scene domain.") + + scene_dict["medium"] = med.perturbed_copy( + **restricted_arrays, interp_method=interp_method + ) + + return Scene.parse_obj(scene_dict) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 57c912a11..0bb5c2134 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -2,71 +2,78 @@ from __future__ import annotations from typing import Dict, Tuple, List, Set, Union -from math import isclose import pydantic.v1 as pydantic import numpy as np import xarray as xr -import matplotlib.pyplot as plt import matplotlib as mpl -from mpl_toolkits.axes_grid1 import make_axes_locatable from .base import cached_property -from .validators import assert_unique_names, assert_objects_in_sim_bounds +from .validators import assert_objects_in_sim_bounds from .validators import validate_mode_objects_symmetry -from .geometry.base import Geometry, Box, GeometryGroup, ClipOperation +from .geometry.base import Geometry, Box from .geometry.primitives import Cylinder from .geometry.mesh import TriangleMesh from .geometry.polyslab import PolySlab from .geometry.utils import flatten_groups, traverse_geometries -from .types import Ax, Shapely, FreqBound, Axis, annotate_type, Symmetry, TYPE_TAG_STR +from .types import Ax, FreqBound, Axis, annotate_type, InterpMethod from .grid.grid import Coords1D, Grid, Coords from .grid.grid_spec import GridSpec, UniformGrid, AutoGrid -from .medium import Medium, MediumType, AbstractMedium, PECMedium +from .medium import Medium, MediumType, AbstractMedium from .medium import AbstractCustomMedium, Medium2D, MediumType3D from .medium import AnisotropicMedium, FullyAnisotropicMedium, AbstractPerturbationMedium from .boundary import BoundarySpec, BlochBoundary, PECBoundary, PMCBoundary, Periodic from .boundary import PML, StablePML, Absorber, AbsorberSpec from .structure import Structure from .source import SourceType, PlaneWave, GaussianBeam, AstigmaticGaussianBeam, CustomFieldSource -from .source import CustomCurrentSource, CustomSourceTime +from .source import CustomCurrentSource, CustomSourceTime, ContinuousWave from .source import TFSF, Source, ModeSource from .monitor import MonitorType, Monitor, FreqMonitor, SurfaceIntegrationMonitor from .monitor import AbstractModeMonitor, FieldMonitor from .monitor import PermittivityMonitor, DiffractionMonitor, AbstractFieldProjectionMonitor +from .monitor import FieldProjectionAngleMonitor, FieldProjectionKSpaceMonitor from .data.dataset import Dataset from .data.data_array import SpatialDataArray from .viz import add_ax_if_none, equal_aspect +from .scene import Scene -from .viz import MEDIUM_CMAP, STRUCTURE_EPS_CMAP, PlotParams, plot_params_symmetry, polygon_path -from .viz import plot_params_structure, plot_params_pml, plot_params_override_structures +from .viz import PlotParams +from .viz import plot_params_pml, plot_params_override_structures from .viz import plot_params_pec, plot_params_pmc, plot_params_bloch, plot_sim_3d -from ..version import __version__ -from ..constants import C_0, SECOND, inf, fp_eps -from ..exceptions import Tidy3dKeyError, SetupError, ValidationError, Tidy3dError +from ..constants import C_0, SECOND, fp_eps +from ..exceptions import SetupError, ValidationError, Tidy3dError, Tidy3dImportError from ..log import log from ..updater import Updater +from .base_sim.simulation import AbstractSimulation + +try: + gdstk_available = True + import gdstk +except ImportError: + gdstk_available = False + +try: + gdspy_available = True + import gdspy +except ImportError: + gdspy_available = False + # minimum number of grid points allowed per central wavelength in a medium MIN_GRIDS_PER_WVL = 6.0 -# maximum number of mediums supported -MAX_NUM_MEDIUMS = 65530 - # maximum number of sources MAX_NUM_SOURCES = 1000 -# maximum geometry count in a single structure -MAX_GEOMETRY_COUNT = 100 - # maximum numbers of simulation parameters MAX_TIME_STEPS = 1e7 WARN_TIME_STEPS = 1e6 MAX_GRID_CELLS = 20e9 MAX_CELLS_TIMES_STEPS = 1e16 WARN_MONITOR_DATA_SIZE_GB = 10 +MAX_MONITOR_INTERNAL_DATA_SIZE_GB = 50 MAX_SIMULATION_DATA_SIZE_GB = 50 WARN_MODE_NUM_CELLS = 1e5 @@ -82,7 +89,7 @@ PML_HEIGHT_FOR_0_DIMS = 0.02 -class Simulation(Box): +class Simulation(AbstractSimulation): """Contains all information about Tidy3d simulation. Example @@ -141,33 +148,6 @@ class Simulation(Box): units=SECOND, ) - medium: MediumType3D = pydantic.Field( - Medium(), - title="Background Medium", - description="Background medium of simulation, defaults to vacuum if not specified.", - discriminator=TYPE_TAG_STR, - ) - - symmetry: Tuple[Symmetry, Symmetry, Symmetry] = pydantic.Field( - (0, 0, 0), - title="Symmetries", - description="Tuple of integers defining reflection symmetry across a plane " - "bisecting the simulation domain normal to the x-, y-, and z-axis " - "at the simulation center of each 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.", - ) - - structures: Tuple[Structure, ...] = pydantic.Field( - (), - title="Structures", - description="Tuple of structures present in simulation. " - "Note: Structures defined later in this list override the " - "simulation material properties in regions of spatial overlap.", - ) - sources: Tuple[annotate_type(SourceType), ...] = pydantic.Field( (), title="Sources", @@ -229,12 +209,6 @@ class Simulation(Box): le=1.0, ) - version: str = pydantic.Field( - __version__, - title="Version", - description="String specifying the front end version number.", - ) - """ Validating setup """ @pydantic.root_validator(pre=True) @@ -256,17 +230,10 @@ def _validate_auto_grid_wavelength(cls, val, values): _ = val.wavelength_from_sources(sources=values.get("sources")) return val - _structures_in_bounds = assert_objects_in_sim_bounds("structures", error=False) _sources_in_bounds = assert_objects_in_sim_bounds("sources") - _monitors_in_bounds = assert_objects_in_sim_bounds("monitors") _mode_sources_symmetries = validate_mode_objects_symmetry("sources") _mode_monitors_symmetries = validate_mode_objects_symmetry("monitors") - # make sure all names are unique - _unique_structure_names = assert_unique_names("structures") - _unique_source_names = assert_unique_names("sources") - _unique_monitor_names = assert_unique_names("monitors") - # _few_enough_mediums = validate_num_mediums() # _structures_not_at_edges = validate_structure_bounds_not_at_edges() # _gap_size_ok = validate_pml_gap_size() @@ -300,7 +267,7 @@ def plane_wave_boundaries(cls, val, values): continue _, tan_dirs = cls.pop_axis([0, 1, 2], axis=source.injection_axis) - medium_set = cls.intersecting_media(source, structures) + medium_set = Scene.intersecting_media(source, structures) medium = medium_set.pop() if medium_set else sim_medium for tan_dir in tan_dirs: @@ -357,7 +324,7 @@ def tfsf_boundaries(cls, val, values): size=temp_size, source_time=source.source_time, ) - medium_set = cls.intersecting_media(temp_src, structures) + medium_set = Scene.intersecting_media(temp_src, structures) medium = medium_set.pop() if medium_set else sim_medium # the source shouldn't touch or cross any boundary in the direction of injection @@ -418,36 +385,19 @@ def tfsf_with_symmetry(cls, val, values): @pydantic.validator("boundary_spec", always=True) def boundaries_for_zero_dims(cls, val, values): - """Warn if an absorbing boundary is used along a zero dimension.""" + """Error if an absorbing boundary is used along a zero dimension.""" boundaries = val.to_list size = values.get("size") for dim, (boundary, size_dim) in enumerate(zip(boundaries, size)): num_absorbing_bdries = sum(isinstance(bnd, AbsorberSpec) for bnd in boundary) if num_absorbing_bdries > 0 and size_dim == 0: - log.warning( - f"If the simulation is intended to be 2D in the plane normal to the " - f"{'xyz'[dim]} axis, using a PML or absorbing boundary along that axis " - f"is incorrect. Consider using a 'Periodic' boundary along {'xyz'[dim]}.", - custom_loc=["boundary_spec", "xyz"[dim]], + raise SetupError( + f"The simulation has zero size along the {'xyz'[dim]} axis, so " + "using a PML or absorbing boundary along that axis is incorrect. " + f"Use either 'Periodic' or 'BlochBoundary' along {'xyz'[dim]}." ) return val - @pydantic.validator("structures", always=True) - def _validate_num_mediums(cls, val): - """Error if too many mediums present.""" - - if val is None: - return val - - mediums = {structure.medium for structure in val} - if len(mediums) > MAX_NUM_MEDIUMS: - raise SetupError( - f"Tidy3d only supports {MAX_NUM_MEDIUMS} distinct mediums." - f"{len(mediums)} were supplied." - ) - - return val - @pydantic.validator("sources", always=True) def _validate_num_sources(cls, val): """Error if too many sources present.""" @@ -464,58 +414,6 @@ def _validate_num_sources(cls, val): return val - @pydantic.validator("structures", always=True) - def _validate_num_geometries(cls, val): - """Error if too many geometries in a single structure.""" - - if val is None: - return val - - for i, structure in enumerate(val): - for geometry in flatten_groups(structure.geometry): - count = sum( - 1 - for g in traverse_geometries(geometry) - if not isinstance(g, (GeometryGroup, ClipOperation)) - ) - if count > MAX_GEOMETRY_COUNT: - raise SetupError( - f"Structure at 'structures[{i}]' has {count} geometries that cannot be " - f"flattened. A maximum of {MAX_GEOMETRY_COUNT} is supported due to " - f"preprocessing performance." - ) - - return val - - @pydantic.validator("structures", always=True) - def _structures_not_at_edges(cls, val, values): - """Warn if any structures lie at the simulation boundaries.""" - - if val is None: - return val - - sim_box = Box(size=values.get("size"), center=values.get("center")) - sim_bound_min, sim_bound_max = sim_box.bounds - sim_bounds = list(sim_bound_min) + list(sim_bound_max) - - with log as consolidated_logger: - for istruct, structure in enumerate(val): - struct_bound_min, struct_bound_max = structure.geometry.bounds - struct_bounds = list(struct_bound_min) + list(struct_bound_max) - - for sim_val, struct_val in zip(sim_bounds, struct_bounds): - - if isclose(sim_val, struct_val): - consolidated_logger.warning( - f"Structure at 'structures[{istruct}]' has bounds that extend exactly " - "to simulation edges. This can cause unexpected behavior. " - "If intending to extend the structure to infinity along one dimension, " - "use td.inf as a size variable instead to make this explicit.", - custom_loc=["structures", istruct], - ) - - return val - @pydantic.validator("structures", always=True) def _validate_2d_geometry_has_2d_medium(cls, val, values): """Warn if a geometry bounding box has zero size in a certain dimension.""" @@ -561,7 +459,7 @@ def warn(istruct, side): consolidated_logger.warning( f"Structure at structures[{istruct}] was detected as being less " f"than half of a central wavelength from a PML on side {side}. " - "To avoid inaccurate results, please increase gap between " + "To avoid inaccurate results or divergence, please increase gap between " "any structures and PML or fully extend structure through the pml.", custom_loc=["structures", istruct], ) @@ -731,7 +629,7 @@ def _projection_monitors_homogeneous(cls, val, values): for monitor in val: if isinstance(monitor, (AbstractFieldProjectionMonitor, DiffractionMonitor)): - mediums = cls.intersecting_media(monitor, total_structures) + mediums = Scene.intersecting_media(monitor, total_structures) # make sure there is no more than one medium in the returned list if len(mediums) > 1: raise SetupError( @@ -741,6 +639,97 @@ def _projection_monitors_homogeneous(cls, val, values): return val + @pydantic.validator("monitors", always=True) + def _projection_direction(cls, val, values): + """Warn if field projection observation points are behind surface projection monitors.""" + # This validator is in simulation.py rather than monitor.py because volume monitors are + # eventually converted to their bounding surface projection monitors, in which case we + # do not want this validator to be triggered. + + if val is None: + return val + + with log as consolidated_logger: + for monitor_ind, monitor in enumerate(val): + if isinstance(monitor, AbstractFieldProjectionMonitor): + if monitor.size.count(0.0) != 1: + continue + + normal_dir = monitor.projection_surfaces[0].normal_dir + normal_ind = monitor.size.index(0.0) + + projecting_backwards = False + if isinstance(monitor, FieldProjectionAngleMonitor): + r, theta, phi = np.meshgrid( + monitor.proj_distance, + monitor.theta, + monitor.phi, + indexing="ij", + ) + x, y, z = Geometry.sph_2_car(r=r, theta=theta, phi=phi) + elif isinstance(monitor, FieldProjectionKSpaceMonitor): + uxs, uys, _ = np.meshgrid( + monitor.ux, + monitor.uy, + monitor.proj_distance, + indexing="ij", + ) + theta, phi = monitor.kspace_2_sph(uxs, uys, monitor.proj_axis) + x, y, z = Geometry.sph_2_car(r=monitor.proj_distance, theta=theta, phi=phi) + else: + pts = monitor.unpop_axis( + monitor.proj_distance, (monitor.x, monitor.y), axis=normal_ind + ) + x, y, z = pts + + center = np.array(monitor.center) - np.array(monitor.local_origin) + pts = [np.array(i) for i in [x, y, z]] + normal_displacement = pts[normal_ind] - center[normal_ind] + if np.any(normal_displacement < 0) and normal_dir == "+": + projecting_backwards = True + elif np.any(normal_displacement > 0) and normal_dir == "-": + projecting_backwards = True + + if projecting_backwards: + consolidated_logger.warning( + f"Field projection monitor '{monitor.name}' has observation points set " + "up such that the monitor is projecting backwards with respect to its " + "'normal_dir'. If this was not intentional, please take a look at the " + "documentation associated with this type of projection monitor to " + "check how the observation point coordinate system is defined.", + custom_loc=["monitors", monitor_ind], + ) + + return val + + @pydantic.validator("monitors", always=True) + def proj_distance_for_approx(cls, val, values): + """Warn if projection distance for projection monitors is not large compared to monitor or, + simulation size, yet far_field_approx is True.""" + if val is None: + return val + + sim_size = values.get("size") + + with log as consolidated_logger: + for monitor_ind, monitor in enumerate(val): + if not isinstance(monitor, AbstractFieldProjectionMonitor): + continue + + name = monitor.name + max_size = min(np.max(monitor.size), np.max(sim_size)) + + if monitor.far_field_approx and np.abs(monitor.proj_distance) < 10 * max_size: + consolidated_logger.warning( + f"Monitor {name} projects to a distance comparable to the size of the " + "monitor; we recommend setting ``far_field_approx=False`` to disable " + "far-field approximations for this monitor, because the approximations " + "are valid only when the observation points are very far compared to the " + "size of the monitor that records near fields.", + custom_loc=["monitors", monitor_ind], + ) + return val + @pydantic.validator("monitors", always=True) def _integration_surfaces_in_bounds(cls, val, values): """Error if any of the integration surfaces are outside of the simulation domain.""" @@ -801,7 +790,7 @@ def diffraction_monitor_medium(cls, val, values): medium = values.get("medium") for monitor in monitors: if isinstance(monitor, DiffractionMonitor): - medium_set = Simulation.intersecting_media(monitor, structures) + medium_set = Scene.intersecting_media(monitor, structures) medium = medium_set.pop() if medium_set else medium _, index_k = medium.nk_model(frequency=np.array(monitor.freqs)) if not np.all(index_k == 0): @@ -827,7 +816,7 @@ def _warn_grid_size_too_small(cls, val, values): for medium_index, medium in enumerate(mediums): # min wavelength in PEC is meaningless and we'll get divide by inf errors - if isinstance(medium, PECMedium): + if medium.is_pec: continue # min wavelength in Medium2D is meaningless if isinstance(medium, Medium2D): @@ -835,9 +824,16 @@ def _warn_grid_size_too_small(cls, val, values): eps_material = medium.eps_model(freq0) n_material, _ = medium.eps_complex_to_nk(eps_material) - lambda_min = C_0 / freq0 / n_material - for key, grid_spec in zip("xyz", (val.grid_x, val.grid_y, val.grid_z)): + for comp, (key, grid_spec) in enumerate( + zip("xyz", (val.grid_x, val.grid_y, val.grid_z)) + ): + if medium.is_pec or ( + isinstance(medium, AnisotropicMedium) and medium.is_comp_pec(comp) + ): + n_material = 1.0 + lambda_min = C_0 / freq0 / n_material + if ( isinstance(grid_spec, UniformGrid) and grid_spec.dl > lambda_min / MIN_GRIDS_PER_WVL @@ -886,7 +882,7 @@ def _source_homogeneous_isotropic(cls, val, values): # for each plane wave in the sources list for source in val: if isinstance(source, (PlaneWave, GaussianBeam, AstigmaticGaussianBeam)): - mediums = cls.intersecting_media(source, total_structures) + mediums = Scene.intersecting_media(source, total_structures) # make sure there is no more than one medium in the returned list if len(mediums) > 1: raise SetupError( @@ -925,14 +921,31 @@ def _check_normalize_index(cls, val, values): if sources[val].source_time.amplitude == 0: raise ValidationError("Cannot set 'normalize_index' to source with zero amplitude.") + # Warn if normalizing by a ContinuousWave or CustomSourceTime source, if frequency-domain monitors are present. + if isinstance(sources[val].source_time, ContinuousWave): + log.warning( + f"'normalize_index' {val} is a source with 'ContinuousWave' " + "time dependence. Normalizing frequency-domain monitors by this " + "source is not meaningful because field decay does not occur. " + "Consider setting 'normalize_index' to 'None' instead." + ) + if isinstance(sources[val].source_time, CustomSourceTime): + log.warning( + f"'normalize_index' {val} is a source with 'CustomSourceTime' " + "time dependence. Normalizing frequency-domain monitors by this " + "source is only meaningful if field decay occurs." + ) + return val """ Post-init validators """ def _post_init_validators(self) -> None: """Call validators taking z`self` that get run after init.""" + _ = self.scene self._validate_no_structures_pml() self._validate_tfsf_nonuniform_grid() + self._validate_nonlinear_specs() def _validate_no_structures_pml(self) -> None: """Ensure no structures terminate / have bounds inside of PML.""" @@ -1004,6 +1017,15 @@ def _validate_tfsf_nonuniform_grid(self) -> None: custom_loc=["sources", source_ind], ) + def _validate_nonlinear_specs(self) -> None: + """Run :class:`.NonlinearSpec` validators that depend on knowing the central + frequencies of the sources.""" + freqs = np.array([source.source_time.freq0 for source in self.sources]) + for medium in self.scene.mediums: + if medium.nonlinear_spec is not None: + for model in medium._nonlinear_models: + model._validate_medium_freqs(medium, freqs) + """ Pre submit validation (before web.upload()) """ def validate_pre_upload(self, source_required: bool = True) -> None: @@ -1063,7 +1085,7 @@ def _validate_monitor_size(self) -> None: with log as consolidated_logger: datas = self.monitors_data_size for monitor_ind, (monitor_name, monitor_size) in enumerate(datas.items()): - monitor_size_gb = monitor_size / 2**30 + monitor_size_gb = monitor_size / 1e9 if monitor_size_gb > WARN_MONITOR_DATA_SIZE_GB: consolidated_logger.warning( f"Monitor '{monitor_name}' estimated storage is {monitor_size_gb:1.2f}GB. " @@ -1080,6 +1102,21 @@ def _validate_monitor_size(self) -> None: f"a maximum of {MAX_SIMULATION_DATA_SIZE_GB:.2f}GB are allowed." ) + # Some monitors store much less data than what is needed internally. Make sure that the + # internal storage also does not exceed the limit. + for monitor in self.monitors: + num_cells = self._monitor_num_cells(monitor) + # intermediate storage needed, in GB + solver_data = monitor._storage_size_solver(num_cells=num_cells, tmesh=self.tmesh) / 1e9 + if solver_data > MAX_MONITOR_INTERNAL_DATA_SIZE_GB: + raise SetupError( + f"Estimated internal storage of monitor '{monitor.name}' is " + f"{solver_data:1.2f}GB, which is larger than the maximum allowed " + f"{MAX_MONITOR_INTERNAL_DATA_SIZE_GB:.2f}GB. Consider making it smaller, " + "using fewer frequencies, or spatial or temporal downsampling using " + "'interval_space' and 'interval', respectively." + ) + def _validate_modes_size(self) -> None: """Warn if mode sources or monitors have a large number of points.""" @@ -1120,19 +1157,20 @@ def warn_mode_size(monitor: AbstractModeMonitor, msg_header: str, custom_loc: Li @cached_property def monitors_data_size(self) -> Dict[str, float]: """Dictionary mapping monitor names to their estimated storage size in bytes.""" - tmesh = self.tmesh data_size = {} for monitor in self.monitors: - name = monitor.name - num_cells = self.discretize_monitor(monitor).num_cells - # take monitor downsampling into account - num_cells = monitor.downsampled_num_cells(num_cells) - num_cells = np.prod(num_cells) - monitor_size = monitor.storage_size(num_cells=num_cells, tmesh=tmesh) - data_size[name] = float(monitor_size) - + num_cells = self._monitor_num_cells(monitor) + storage_size = float(monitor.storage_size(num_cells=num_cells, tmesh=self.tmesh)) + data_size[monitor.name] = storage_size return data_size + def _monitor_num_cells(self, monitor: Monitor) -> int: + """Total number of cells included by monitor based on simulation grid.""" + num_cells = self.discretize_monitor(monitor).num_cells + # take monitor downsampling into account + num_cells = monitor.downsampled_num_cells(num_cells) + return np.prod(np.array(num_cells, dtype=np.int64)) + def _validate_datasets_not_none(self) -> None: """Ensures that all custom datasets are defined.""" if any(dataset is None for dataset in self.custom_datasets): @@ -1165,7 +1203,7 @@ def _validate_tfsf_structure_intersections(self) -> None: if surface.name[-2] != "xyz"[source.injection_axis]: sidewall_surfaces.append(surface) - intersecting_structs = self.intersecting_structures( + intersecting_structs = Scene.intersecting_structures( test_object=surface, structures=self.structures ) @@ -1223,6 +1261,7 @@ def _validate_tfsf_structure_intersections(self) -> None: """ Accounting """ + # candidate for removal in 3.0 @cached_property def mediums(self) -> Set[MediumType]: """Returns set of distinct :class:`.AbstractMedium` in simulation. @@ -1232,10 +1271,13 @@ def mediums(self) -> Set[MediumType]: List[:class:`.AbstractMedium`] Set of distinct mediums in the simulation. """ - medium_dict = {self.medium: None} - medium_dict.update({structure.medium: None for structure in self.structures}) - return list(medium_dict.keys()) + log.warning( + "'Simulation.mediums' will be removed in Tidy3D 3.0. " + "Use 'Simulation.scene.mediums' instead." + ) + return self.scene.mediums + # candidate for removal in 3.0 @cached_property def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: """Returns dict mapping medium to index in material. @@ -1248,21 +1290,24 @@ def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: Mapping between distinct mediums to index in simulation. """ - return {medium: index for index, medium in enumerate(self.mediums)} - - def get_monitor_by_name(self, name: str) -> Monitor: - """Return monitor named 'name'.""" - for monitor in self.monitors: - if monitor.name == name: - return monitor - raise Tidy3dKeyError(f"No monitor named '{name}'") + log.warning( + "'Simulation.medium_map' will be removed in Tidy3D 3.0. " + "Use 'Simulation.scene.medium_map' instead." + ) + return self.scene.medium_map + # candidate for removal in 3.0 @cached_property def background_structure(self) -> Structure: """Returns structure representing the background of the :class:`.Simulation`.""" - geometry = Box(size=(inf, inf, inf)) - return Structure(geometry=geometry, medium=self.medium) + log.warning( + "'Simulation.background_structure' will be removed in Tidy3D 3.0. " + "Use 'Simulation.scene.background_structure' instead." + ) + return self.scene.background_structure + + # candidate for removal in 3.0 @staticmethod def intersecting_media( test_object: Box, structures: Tuple[Structure, ...] @@ -1283,20 +1328,14 @@ def intersecting_media( List[:class:`.AbstractMedium`] Set of distinct mediums that intersect with the given planar object. """ - if test_object.size.count(0.0) == 1: - # get all merged structures on the test_object, which is already planar - structures_merged = Simulation._filter_structures_plane(structures, test_object) - mediums = {medium for medium, _ in structures_merged} - return mediums - - # if the test object is a volume, test each surface recursively - surfaces = test_object.surfaces_with_exclusion(**test_object.dict()) - mediums = set() - for surface in surfaces: - _mediums = Simulation.intersecting_media(surface, structures) - mediums.update(_mediums) - return mediums + log.warning( + "'Simulation.intersecting_media()' will be removed in Tidy3D 3.0. " + "Use 'Scene.intersecting_media()' instead." + ) + return Scene.intersecting_media(test_object=test_object, structures=structures) + + # candidate for removal in 3.0 @staticmethod def intersecting_structures( test_object: Box, structures: Tuple[Structure, ...] @@ -1317,26 +1356,12 @@ def intersecting_structures( Set of distinct structures that intersect with the given surface, or with the surfaces of the given volume. """ - if test_object.size.count(0.0) == 1: - # get all merged structures on the test_object, which is already planar - normal_axis_index = test_object.size.index(0.0) - dim = "xyz"[normal_axis_index] - pos = test_object.center[normal_axis_index] - xyz_kwargs = {dim: pos} - - structures_merged = [] - for structure in structures: - intersections = structure.geometry.intersections_plane(**xyz_kwargs) - if len(intersections) > 0: - structures_merged.append(structure) - return structures_merged - - # if the test object is a volume, test each surface recursively - surfaces = test_object.surfaces_with_exclusion(**test_object.dict()) - structures_merged = [] - for surface in surfaces: - structures_merged += Simulation.intersecting_structures(surface, structures) - return structures_merged + + log.warning( + "'Simulation.intersecting_structures()' will be removed in Tidy3D 3.0. " + "Use 'Scene.intersecting_structures()' instead." + ) + return Scene.intersecting_structures(test_object=test_object, structures=structures) def monitor_medium(self, monitor: MonitorType): """Return the medium in which the given monitor resides. @@ -1351,7 +1376,7 @@ def monitor_medium(self, monitor: MonitorType): :class:`.AbstractMedium` Medium associated with the given :class:`.Monitor`. """ - medium_set = self.intersecting_media(monitor, self.structures) + medium_set = Scene.intersecting_media(monitor, self.structures) if len(medium_set) > 1: raise SetupError(f"Monitor '{monitor.name}' intersects more than one medium.") medium = medium_set.pop() if medium_set else self.medium @@ -1401,6 +1426,268 @@ def _check_bloch_vec( custom_loc=["boundary_spec", "xyz"[dim]], ) + def to_gdstk( + self, + x: float = None, + y: float = None, + z: float = None, + permittivity_threshold: pydantic.NonNegativeFloat = 1, + frequency: pydantic.PositiveFloat = 0, + gds_layer_dtype_map: Dict[ + AbstractMedium, Tuple[pydantic.NonNegativeInt, pydantic.NonNegativeInt] + ] = None, + ) -> List: + """Convert a simulation's planar slice to a .gds type polygon list. + + 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. + permittivity_threshold : float = 1.001 + Permitivitty value used to define the shape boundaries for structures with custom + medim + frequency : float = 0 + Frequency for permittivity evaluaiton in case of custom medium (Hz). + gds_layer_dtype_map : Dict + Dictionary mapping mediums to GDSII layer and data type tuples. + + Return + ------ + List + List of `gdstk.Polygon`. + """ + axis, _ = self.geometry.parse_xyz_kwargs(x=x, y=y, z=z) + _, bmin = self.pop_axis(self.bounds[0], axis) + _, bmax = self.pop_axis(self.bounds[1], axis) + + _, symmetry = self.pop_axis(self.symmetry, axis) + if symmetry[0] != 0: + bmin = (0, bmin[1]) + if symmetry[1] != 0: + bmin = (bmin[0], 0) + clip = gdstk.rectangle(bmin, bmax) + + polygons = [] + for structure in self.structures: + gds_layer, gds_dtype = gds_layer_dtype_map.get(structure.medium, (0, 0)) + for polygon in structure.to_gdstk( + x=x, + y=y, + z=z, + permittivity_threshold=permittivity_threshold, + frequency=frequency, + gds_layer=gds_layer, + gds_dtype=gds_dtype, + ): + pmin, pmax = polygon.bounding_box() + if pmin[0] < bmin[0] or pmin[1] < bmin[1] or pmax[0] > bmax[0] or pmax[1] > bmax[1]: + polygons.extend( + gdstk.boolean(clip, polygon, "and", layer=gds_layer, datatype=gds_dtype) + ) + else: + polygons.append(polygon) + + return polygons + + def to_gdspy( + self, + x: float = None, + y: float = None, + z: float = None, + gds_layer_dtype_map: Dict[ + AbstractMedium, Tuple[pydantic.NonNegativeInt, pydantic.NonNegativeInt] + ] = None, + ) -> List: + """Convert a simulation's planar slice to a .gds type polygon list. + + 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. + gds_layer_dtype_map : Dict + Dictionary mapping mediums to GDSII layer and data type tuples. + + Return + ------ + List + List of `gdspy.Polygon` and `gdspy.PolygonSet`. + """ + axis, _ = self.geometry.parse_xyz_kwargs(x=x, y=y, z=z) + _, bmin = self.pop_axis(self.bounds[0], axis) + _, bmax = self.pop_axis(self.bounds[1], axis) + + _, symmetry = self.pop_axis(self.symmetry, axis) + if symmetry[0] != 0: + bmin = (0, bmin[1]) + if symmetry[1] != 0: + bmin = (bmin[0], 0) + clip = gdspy.Rectangle(bmin, bmax) + + polygons = [] + for structure in self.structures: + gds_layer, gds_dtype = gds_layer_dtype_map.get(structure.medium, (0, 0)) + for polygon in structure.to_gdspy( + x=x, + y=y, + z=z, + gds_layer=gds_layer, + gds_dtype=gds_dtype, + ): + pmin, pmax = polygon.get_bounding_box() + if pmin[0] < bmin[0] or pmin[1] < bmin[1] or pmax[0] > bmax[0] or pmax[1] > bmax[1]: + polygon = gdspy.boolean( + clip, polygon, "and", layer=gds_layer, datatype=gds_dtype + ) + polygons.append(polygon) + return polygons + + def to_gds( + self, + cell, + x: float = None, + y: float = None, + z: float = None, + permittivity_threshold: pydantic.NonNegativeFloat = 1, + frequency: pydantic.PositiveFloat = 0, + gds_layer_dtype_map: Dict[ + AbstractMedium, Tuple[pydantic.NonNegativeInt, pydantic.NonNegativeInt] + ] = None, + ) -> None: + """Append the simulation structures to a .gds cell. + + Parameters + ---------- + cell : ``gdstk.Cell`` or ``gdspy.Cell`` + Cell object to which the generated polygons are added. + 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. + permittivity_threshold : float = 1.001 + Permitivitty value used to define the shape boundaries for structures with custom + medim + frequency : float = 0 + Frequency for permittivity evaluaiton in case of custom medium (Hz). + gds_layer_dtype_map : Dict + Dictionary mapping mediums to GDSII layer and data type tuples. + """ + if gds_layer_dtype_map is None: + gds_layer_dtype_map = {} + + if gdstk_available and isinstance(cell, gdstk.Cell): + polygons = self.to_gdstk( + x=x, + y=y, + z=z, + permittivity_threshold=permittivity_threshold, + frequency=frequency, + gds_layer_dtype_map=gds_layer_dtype_map, + ) + if len(polygons) > 0: + cell.add(*polygons) + + elif gdspy_available and isinstance(cell, gdspy.Cell): + polygons = self.to_gdspy(x=x, y=y, z=z, gds_layer_dtype_map=gds_layer_dtype_map) + if len(polygons) > 0: + cell.add(polygons) + + elif "gdstk" in cell.__class__ and not gdstk_available: + raise Tidy3dImportError( + "Module 'gdstk' not found. It is required to export shapes to gdstk cells." + ) + elif "gdspy" in cell.__class__ and not gdspy_available: + raise Tidy3dImportError( + "Module 'gdspy' not found. It is required to export shapes to gdspy cells." + ) + else: + raise Tidy3dError( + "Argument 'cell' must be an instance of 'gdstk.Cell' or 'gdspy.Cell'." + ) + + def to_gds_file( + self, + fname: str, + x: float = None, + y: float = None, + z: float = None, + permittivity_threshold: pydantic.NonNegativeFloat = 1, + frequency: pydantic.PositiveFloat = 0, + gds_layer_dtype_map: Dict[ + AbstractMedium, Tuple[pydantic.NonNegativeInt, pydantic.NonNegativeInt] + ] = None, + gds_cell_name: str = "MAIN", + ) -> None: + """Append the simulation structures to a .gds cell. + + Parameters + ---------- + fname : str + Full path to the .gds file to save the :class:`Simulation` slice to. + 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. + permittivity_threshold : float = 1.001 + Permitivitty value used to define the shape boundaries for structures with custom + medim + frequency : float = 0 + Frequency for permittivity evaluaiton in case of custom medium (Hz). + gds_layer_dtype_map : Dict + Dictionary mapping mediums to GDSII layer and data type tuples. + gds_cell_name : str = 'MAIN' + Name of the cell created in the .gds file to store the geometry. + """ + if gdstk_available: + library = gdstk.Library() + reference = gdstk.Reference + rotation = np.pi + elif gdspy_available: + library = gdspy.GdsLibrary() + reference = gdspy.CellReference + rotation = 180 + else: + raise Tidy3dImportError( + "Python modules 'gdspy' and 'gdstk' not found. To export geometries to .gds " + "files, please install one of those those modules." + ) + cell = library.new_cell(gds_cell_name) + + axis, _ = self.geometry.parse_xyz_kwargs(x=x, y=y, z=z) + _, symmetry = self.pop_axis(self.symmetry, axis) + if symmetry[0] != 0: + outer_cell = cell + cell = library.new_cell(gds_cell_name + "_X") + outer_cell.add(reference(cell)) + outer_cell.add(reference(cell, rotation=rotation, x_reflection=True)) + if symmetry[1] != 0: + outer_cell = cell + cell = library.new_cell(gds_cell_name + "_Y") + outer_cell.add(reference(cell)) + outer_cell.add(reference(cell, x_reflection=True)) + + self.to_gds( + cell, + x=x, + y=y, + z=z, + permittivity_threshold=permittivity_threshold, + frequency=frequency, + gds_layer_dtype_map=gds_layer_dtype_map, + ) + library.write_gds(fname) + """ Plotting """ @equal_aspect @@ -1443,28 +1730,18 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - # if no hlim and/or vlim given, the bounds will then be the usual pml bounds - axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (hmin, vmin) = self.pop_axis(self.bounds_pml[0], axis=axis) - _, (hmax, vmax) = self.pop_axis(self.bounds_pml[1], axis=axis) - - # account for unordered limits - if hlim is None: - hlim = (hmin, hmax) - if vlim is None: - vlim = (vmin, vmax) - - if hlim[0] > hlim[1]: - raise Tidy3dError("Error: 'hmin' > 'hmax'") - if vlim[0] > vlim[1]: - raise Tidy3dError("Error: 'vmin' > 'vmax'") + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) ax = self.plot_structures(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) ax = self.plot_pml(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) return ax @@ -1516,121 +1793,32 @@ def plot_eps( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - # if no hlim and/or vlim given, the bounds will then be the usual pml bounds - axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (hmin, vmin) = self.pop_axis(self.bounds_pml[0], axis=axis) - _, (hmax, vmax) = self.pop_axis(self.bounds_pml[1], axis=axis) - - # account for unordered limits - if hlim is None: - hlim = (hmin, hmax) - if vlim is None: - vlim = (vmin, vmax) - if hlim[0] > hlim[1]: - raise Tidy3dError("Error: hmin > hmax") - if vlim[0] > vlim[1]: - raise Tidy3dError("Error: vmin > vmax") + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) ax = self.plot_structures_eps( - freq=freq, cbar=True, alpha=alpha, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + freq=freq, + cbar=True, + alpha=alpha, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, ) ax = self.plot_sources(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=source_alpha) ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim, alpha=monitor_alpha) ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) ax = self.plot_pml(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) - return ax - - @equal_aspect - @add_ax_if_none - def plot_structures( - self, - x: float = None, - y: float = None, - z: float = None, - ax: Ax = None, - hlim: Tuple[float, float] = None, - vlim: Tuple[float, float] = None, - ) -> Ax: - """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. - hlim : Tuple[float, float] = None - The x range if plotting on xy or xz planes, y range if plotting on yz plane. - vlim : Tuple[float, float] = None - The z range if plotting on xz or yz planes, y plane if plotting on xy plane. - - Returns - ------- - matplotlib.axes._subplots.Axes - The supplied or created matplotlib axes. - """ - - medium_shapes = self._get_structures_2dbox( - structures=self.structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) - medium_map = self.medium_map - for medium, shape in medium_shapes: - mat_index = medium_map[medium] - ax = self._plot_shape_structure(medium=medium, mat_index=mat_index, shape=shape, ax=ax) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - - # clean up the axis display - axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) - ax = self.add_ax_labels_lims(axis=axis, ax=ax) - ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") - - return ax - - def _plot_shape_structure(self, medium: Medium, mat_index: int, shape: Shapely, ax: Ax) -> Ax: - """Plot a structure's cross section shape for a given medium.""" - plot_params_struct = self._get_structure_plot_params(medium=medium, mat_index=mat_index) - ax = self.plot_shape(shape=shape, plot_params=plot_params_struct, ax=ax) + ax = self.plot_boundaries(ax=ax, x=x, y=y, z=z) return ax - def _get_structure_plot_params(self, mat_index: int, medium: Medium) -> PlotParams: - """Constructs the plot parameters for a given medium in simulation.plot().""" - - plot_params = plot_params_structure.copy(update={"linewidth": 0}) - - if mat_index == 0 or medium == self.medium: - # background medium - plot_params = plot_params.copy(update={"facecolor": "white", "edgecolor": "white"}) - elif isinstance(medium, PECMedium): - # perfect electrical conductor - plot_params = plot_params.copy( - update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1} - ) - elif isinstance(medium, Medium2D): - # 2d material - plot_params = plot_params.copy(update={"edgecolor": "k", "linewidth": 1}) - else: - # regular medium - facecolor = MEDIUM_CMAP[(mat_index - 1) % len(MEDIUM_CMAP)] - plot_params = plot_params.copy(update={"facecolor": facecolor}) - - return plot_params - - @staticmethod - def _add_cbar(eps_min: float, eps_max: float, ax: Ax = None) -> None: - """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) - mappable = mpl.cm.ScalarMappable(norm=norm, cmap=STRUCTURE_EPS_CMAP) - plt.colorbar(mappable, cax=cax, label=r"$\epsilon_r$") - @equal_aspect @add_ax_if_none def plot_structures_eps( @@ -1681,292 +1869,33 @@ def plot_structures_eps( The supplied or created matplotlib axes. """ - structures = self.structures + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) - # alpha is None just means plot without any transparency - if alpha is None: - alpha = 1 + return self.scene.plot_structures_eps( + freq=freq, + cbar=cbar, + alpha=alpha, + ax=ax, + x=x, + y=y, + z=z, + hlim=hlim, + vlim=vlim, + grid=self.grid, + reverse=reverse, + ) - if alpha <= 0: - return ax + # candidate for removal in 3.0 + def eps_bounds(self, freq: float = None) -> Tuple[float, float]: + """Compute range of (real) permittivity present in the simulation at frequency "freq".""" - if alpha < 1 and not isinstance(self.medium, AbstractCustomMedium): - axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) - center = Box.unpop_axis(position, (0, 0), axis=axis) - size = Box.unpop_axis(0, (inf, inf), axis=axis) - plane = Box(center=center, size=size) - medium_shapes = self._filter_structures_plane(structures=structures, plane=plane) - else: - structures = [self.background_structure] + list(structures) - medium_shapes = self._get_structures_2dbox( - structures=structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim - ) - - eps_min, eps_max = self.eps_bounds(freq=freq) - for medium, shape in medium_shapes: - # if the background medium is custom medium, it needs to be rendered separately - if medium == self.medium and alpha < 1 and not isinstance(medium, AbstractCustomMedium): - continue - # no need to add patches for custom medium - if not isinstance(medium, AbstractCustomMedium): - ax = self._plot_shape_structure_eps( - freq=freq, - alpha=alpha, - medium=medium, - eps_min=eps_min, - eps_max=eps_max, - reverse=reverse, - shape=shape, - ax=ax, - ) - else: - # For custom medium, apply pcolormesh clipped by the shape. - self._pcolormesh_shape_custom_medium_structure_eps( - x, y, z, freq, alpha, medium, eps_min, eps_max, reverse, shape, ax - ) - - 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, hlim=hlim, vlim=vlim) - - # clean up the axis display - axis, position = self.parse_xyz_kwargs(x=x, y=y, z=z) - ax = self.add_ax_labels_lims(axis=axis, ax=ax) - ax.set_title(f"cross section at {'xyz'[axis]}={position:.2f}") - - return ax - - def eps_bounds(self, freq: float = None) -> Tuple[float, float]: - """Compute range of (real) permittivity present in the simulation at frequency "freq".""" - - medium_list = [self.medium] + list(self.mediums) - medium_list = [medium for medium in medium_list if not isinstance(medium, PECMedium)] - # regular medium - eps_list = [ - medium.eps_model(freq).real - for medium in medium_list - if not isinstance(medium, AbstractCustomMedium) and not isinstance(medium, Medium2D) - ] - eps_min = min(eps_list, default=1) - eps_max = max(eps_list, default=1) - # custom medium, the min and max in the supplied dataset over all components and - # spatial locations. - for mat in [medium for medium in medium_list if isinstance(medium, AbstractCustomMedium)]: - eps_dataarray = mat.eps_dataarray_freq(freq) - eps_min = min( - eps_min, - min(np.min(eps_comp.real.values.ravel()) for eps_comp in eps_dataarray), - ) - eps_max = max( - eps_max, - max(np.max(eps_comp.real.values.ravel()) for eps_comp in eps_dataarray), - ) - return eps_min, eps_max - - def _pcolormesh_shape_custom_medium_structure_eps( - self, - x: float, - y: float, - z: float, - freq: float, - alpha: float, - medium: Medium, - eps_min: float, - eps_max: float, - reverse: bool, - shape: Shapely, - ax: Ax, - ): - """ - Plot shape made of custom medium with ``pcolormesh``. - """ - coords = "xyz" - normal_axis_ind, normal_position = self.parse_xyz_kwargs(x=x, y=y, z=z) - normal_axis, plane_axes = self.pop_axis(coords, normal_axis_ind) - - # First, obtain `span_inds` of grids for interpolating permittivity in the - # bounding box of the shape - shape_bounds = shape.bounds - rmin, rmax = [*shape_bounds[:2]], [*shape_bounds[2:]] - rmin.insert(normal_axis_ind, normal_position) - rmax.insert(normal_axis_ind, normal_position) - span_inds = self.grid.discretize_inds(Box.from_bounds(rmin=rmin, rmax=rmax), extend=True) - # filter negative or too large inds - n_grid = [len(grid_comp) for grid_comp in self.grid.boundaries.to_list] - span_inds = [ - (max(fmin, 0), min(fmax, n_grid[f_ind])) for f_ind, (fmin, fmax) in enumerate(span_inds) - ] - - # assemble the coordinate in the 2d plane - plane_coord = [] - for plane_axis in range(2): - ind_axis = "xyz".index(plane_axes[plane_axis]) - plane_coord.append(self.grid.boundaries.to_list[ind_axis][slice(*span_inds[ind_axis])]) - - # prepare `Coords` for interpolation - coord_dict = { - plane_axes[0]: plane_coord[0], - plane_axes[1]: plane_coord[1], - normal_axis: [normal_position], - } - coord_shape = Coords(**coord_dict) - # interpolate permittivity and take the average over components - eps_shape = np.mean(medium.eps_diagonal_on_grid(frequency=freq, coords=coord_shape), axis=0) - # remove the normal_axis and take real part - eps_shape = eps_shape.real.mean(axis=normal_axis_ind) - # reverse - if reverse: - eps_shape = eps_min + eps_max - eps_shape - - # pcolormesh - plane_xp, plane_yp = np.meshgrid(plane_coord[0], plane_coord[1], indexing="ij") - ax.pcolormesh( - plane_xp, - plane_yp, - eps_shape, - clip_path=(polygon_path(shape), ax.transData), - cmap=STRUCTURE_EPS_CMAP, - vmin=eps_min, - vmax=eps_max, - alpha=alpha, - clip_box=ax.bbox, - ) - - def _get_structure_eps_plot_params( - self, - medium: Medium, - freq: float, - eps_min: float, - eps_max: float, - reverse: bool = False, - alpha: float = None, - ) -> PlotParams: - """Constructs the plot parameters for a given medium in simulation.plot_eps().""" - - plot_params = plot_params_structure.copy(update={"linewidth": 0}) - if alpha is not None: - plot_params = plot_params.copy(update={"alpha": alpha}) - - if isinstance(medium, PECMedium): - # perfect electrical conductor - plot_params = plot_params.copy( - update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1} - ) - elif isinstance(medium, Medium2D): - # 2d material - plot_params = plot_params.copy(update={"edgecolor": "k", "linewidth": 1}) - else: - # regular medium - eps_medium = medium.eps_model(frequency=freq).real - delta_eps = eps_medium - eps_min - delta_eps_max = eps_max - eps_min + 1e-5 - eps_fraction = delta_eps / delta_eps_max - color = eps_fraction if reverse else 1 - eps_fraction - plot_params = plot_params.copy(update={"facecolor": str(color)}) - - return plot_params - - def _plot_shape_structure_eps( - self, - freq: float, - medium: Medium, - shape: Shapely, - eps_min: float, - eps_max: float, - ax: Ax, - reverse: bool = False, - alpha: float = None, - ) -> Ax: - """Plot a structure's cross section shape for a given medium, grayscale for permittivity.""" - plot_params = self._get_structure_eps_plot_params( - medium=medium, freq=freq, eps_min=eps_min, eps_max=eps_max, alpha=alpha, reverse=reverse - ) - ax = self.plot_shape(shape=shape, plot_params=plot_params, ax=ax) - return ax - - @equal_aspect - @add_ax_if_none - def plot_sources( - self, - x: float = None, - y: float = None, - z: float = None, - hlim: Tuple[float, float] = None, - vlim: Tuple[float, float] = None, - alpha: float = None, - ax: Ax = None, - ) -> Ax: - """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. - hlim : Tuple[float, float] = None - The x range if plotting on xy or xz planes, y range if plotting on yz plane. - vlim : Tuple[float, float] = None - The z range if plotting on xz or yz planes, y plane if plotting on xy plane. - alpha : float = None - Opacity of the sources, If ``None`` uses Tidy3d default. - 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. - """ - bounds = self.bounds - for source in self.sources: - ax = source.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - return ax - - @equal_aspect - @add_ax_if_none - def plot_monitors( - self, - x: float = None, - y: float = None, - z: float = None, - hlim: Tuple[float, float] = None, - vlim: Tuple[float, float] = None, - alpha: float = None, - ax: Ax = None, - ) -> Ax: - """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. - hlim : Tuple[float, float] = None - The x range if plotting on xy or xz planes, y range if plotting on yz plane. - vlim : Tuple[float, float] = None - The z range if plotting on xz or yz planes, y plane if plotting on xy plane. - alpha : float = None - Opacity of the sources, If ``None`` uses Tidy3d default. - 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. - """ - bounds = self.bounds - for monitor in self.monitors: - ax = monitor.plot(x=x, y=y, z=z, alpha=alpha, ax=ax, sim_bounds=bounds) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - return ax + log.warning( + "'Simulation.eps_bounds()' will be removed in Tidy3D 3.0. " + "Use 'Simulation.scene.eps_bounds()' instead." + ) + return self.scene.eps_bounds(freq=freq) @cached_property def num_pml_layers(self) -> List[Tuple[float, float]]: @@ -2003,8 +1932,18 @@ def pml_thicknesses(self) -> List[Tuple[float, float]]: pml_thicknesses.append((thick_l, thick_r)) return pml_thicknesses + # candidate for removal in 3.0 @cached_property def bounds_pml(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: + """Simulation bounds including the PML regions.""" + log.warning( + "'Simulation.bounds_pml' will be removed in Tidy3D 3.0. " + "Use 'Simulation.simulation_bounds' instead." + ) + return self.simulation_bounds + + @cached_property + def simulation_bounds(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: """Simulation bounds including the PML regions.""" pml_thick = self.pml_thicknesses bounds_in = self.bounds @@ -2013,14 +1952,6 @@ def bounds_pml(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, fl return (bounds_min, bounds_max) - @cached_property - def simulation_geometry(self) -> Box: - """The entire simulation domain including PML layers. It is identical to - ``sim.geometry`` in the absence of PML. - """ - rmin, rmax = self.bounds_pml - return Box.from_bounds(rmin=rmin, rmax=rmax) - @equal_aspect @add_ax_if_none def plot_pml( @@ -2059,7 +1990,9 @@ def plot_pml( pml_boxes = self._make_pml_boxes(normal_axis=normal_axis) for pml_box in pml_boxes: pml_box.plot(x=x, y=y, z=z, ax=ax, **plot_params_pml.to_kwargs()) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) return ax def _make_pml_boxes(self, normal_axis: Axis) -> List[Box]: @@ -2078,7 +2011,7 @@ def _make_pml_boxes(self, normal_axis: Axis) -> List[Box]: def _make_pml_box(self, pml_axis: Axis, pml_height: float, sign: int) -> Box: """Construct a :class:`.Box` representing an arborbing boundary to be plotted.""" - rmin, rmax = (list(bounds) for bounds in self.bounds_pml) + rmin, rmax = (list(bounds) for bounds in self.simulation_bounds) if sign == -1: rmax[pml_axis] = rmin[pml_axis] + pml_height else: @@ -2094,77 +2027,6 @@ def _make_pml_box(self, pml_axis: Axis, pml_height: float, sign: int) -> Box: return pml_box - @equal_aspect - @add_ax_if_none - def plot_symmetries( - self, - x: float = None, - y: float = None, - z: float = None, - hlim: Tuple[float, float] = None, - vlim: Tuple[float, float] = None, - ax: Ax = None, - ) -> Ax: - """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. - hlim : Tuple[float, float] = None - The x range if plotting on xy or xz planes, y range if plotting on yz plane. - vlim : Tuple[float, float] = None - The z range if plotting on xz or yz planes, y plane if plotting on xy 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. - """ - - normal_axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - - for sym_axis, sym_value in enumerate(self.symmetry): - if sym_value == 0 or sym_axis == normal_axis: - continue - sym_box = self._make_symmetry_box(sym_axis=sym_axis) - plot_params = self._make_symmetry_plot_params(sym_value=sym_value) - ax = sym_box.plot(x=x, y=y, z=z, ax=ax, **plot_params.to_kwargs()) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) - return ax - - def _make_symmetry_plot_params(self, sym_value: Symmetry) -> PlotParams: - """Make PlotParams for symmetry.""" - - plot_params = plot_params_symmetry.copy() - - if sym_value == 1: - plot_params = plot_params.copy( - update={"facecolor": "lightsteelblue", "edgecolor": "lightsteelblue", "hatch": "++"} - ) - elif sym_value == -1: - plot_params = plot_params.copy( - update={"facecolor": "goldenrod", "edgecolor": "goldenrod", "hatch": "--"} - ) - - return plot_params - - def _make_symmetry_box(self, sym_axis: Axis) -> Box: - """Construct a :class:`.Box` representing the symmetry to be plotted.""" - sym_box = self.simulation_geometry - size = list(sym_box.size) - size[sym_axis] /= 2 - center = list(sym_box.center) - center[sym_axis] -= size[sym_axis] / 2 - - return Box(size=size, center=center) - @add_ax_if_none def plot_grid( self, @@ -2209,8 +2071,8 @@ def plot_grid( _, (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]] - _, (xmin, ymin) = self.pop_axis(self.bounds_pml[0], axis=axis) - _, (xmax, ymax) = self.pop_axis(self.bounds_pml[1], axis=axis) + _, (xmin, ymin) = self.pop_axis(self.simulation_bounds[0], axis=axis) + _, (xmax, ymax) = self.pop_axis(self.simulation_bounds[1], axis=axis) segs_x = [((bound, ymin), (bound, ymax)) for bound in boundaries_x] line_segments_x = mpl.collections.LineCollection(segs_x, **kwargs) segs_y = [((xmin, bound), (xmax, bound)) for bound in boundaries_y] @@ -2236,7 +2098,9 @@ def plot_grid( ) ax.add_patch(rect) - ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim) + ax = Scene._set_plot_bounds( + bounds=self.simulation_bounds, ax=ax, x=x, y=y, z=z, hlim=hlim, vlim=vlim + ) return ax @@ -2357,217 +2221,6 @@ def set_plot_params(boundary_edge, lim, side, thickness): return ax - def _set_plot_bounds( - self, - ax: Ax, - x: float = None, - y: float = None, - z: float = None, - hlim: Tuple[float, float] = None, - vlim: Tuple[float, 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. - hlim : Tuple[float, float] = None - The x range if plotting on xy or xz planes, y range if plotting on yz plane. - vlim : Tuple[float, float] = None - The z range if plotting on xz or yz planes, y plane if plotting on xy 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) = self.pop_axis(self.bounds_pml[0], axis=axis) - _, (xmax, ymax) = self.pop_axis(self.bounds_pml[1], axis=axis) - - if hlim is not None: - (xmin, xmax) = hlim - if vlim is not None: - (ymin, ymax) = vlim - - if xmin != xmax: - ax.set_xlim(xmin, xmax) - if ymin != ymax: - ax.set_ylim(ymin, ymax) - - return ax - - @staticmethod - def _get_structures_plane( - structures: List[Structure], x: float = None, y: float = None, z: float = None - ) -> List[Tuple[Medium, Shapely]]: - """Compute list of shapes to plot on plane specified by {x,y,z}. - - Parameters - ---------- - structures : List[:class:`.Structure`] - list of structures to filter on the plane. - 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:`.AbstractMedium`, shapely.geometry.base.BaseGeometry]] - List of shapes and mediums on the plane. - """ - medium_shapes = [] - for structure in structures: - intersections = structure.geometry.intersections_plane(x=x, y=y, z=z) - if len(intersections) > 0: - for shape in intersections: - shape = Geometry.evaluate_inf_shape(shape) - medium_shapes.append((structure.medium, shape)) - return medium_shapes - - def _get_structures_2dbox( - self, - structures: List[Structure], - x: float = None, - y: float = None, - z: float = None, - hlim: Tuple[float, float] = None, - vlim: Tuple[float, float] = None, - ) -> List[Tuple[Medium, Shapely]]: - """Compute list of shapes to plot on 2d box specified by (x_min, x_max), (y_min, y_max). - - Parameters - ---------- - structures : List[:class:`.Structure`] - list of structures to filter on the plane. - 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. - hlim : Tuple[float, float] = None - The x range if plotting on xy or xz planes, y range if plotting on yz plane. - vlim : Tuple[float, float] = None - The z range if plotting on xz or yz planes, y plane if plotting on xy plane. - - Returns - ------- - List[Tuple[:class:`.AbstractMedium`, shapely.geometry.base.BaseGeometry]] - List of shapes and mediums on the plane. - """ - # if no hlim and/or vlim given, the bounds will then be the usual pml bounds - axis, _ = self.parse_xyz_kwargs(x=x, y=y, z=z) - _, (hmin, vmin) = self.pop_axis(self.bounds_pml[0], axis=axis) - _, (hmax, vmax) = self.pop_axis(self.bounds_pml[1], axis=axis) - - if hlim is not None: - (hmin, hmax) = hlim - if vlim is not None: - (vmin, vmax) = vlim - - # get center and size with h, v - h_center = (hmin + hmax) / 2.0 - v_center = (vmin + vmax) / 2.0 - h_size = (hmax - hmin) or inf - v_size = (vmax - vmin) or inf - - axis, center_normal = self.parse_xyz_kwargs(x=x, y=y, z=z) - center = self.unpop_axis(center_normal, (h_center, v_center), axis=axis) - size = self.unpop_axis(0.0, (h_size, v_size), axis=axis) - plane = Box(center=center, size=size) - - medium_shapes = [] - for structure in structures: - intersections = plane.intersections_with(structure.geometry) - for shape in intersections: - if not shape.is_empty: - shape = Box.evaluate_inf_shape(shape) - medium_shapes.append((structure.medium, shape)) - return medium_shapes - - @staticmethod - def _filter_structures_plane( - structures: List[Structure], plane: Box - ) -> List[Tuple[Medium, Shapely]]: - """Compute list of shapes to plot on plane specified by {x,y,z}. - Overlaps are removed or merged depending on medium. - - Parameters - ---------- - structures : List[:class:`.Structure`] - list of structures to filter on the plane. - 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:`.AbstractMedium`, shapely.geometry.base.BaseGeometry]] - List of shapes and mediums on the plane after merging. - """ - - shapes = [] - for structure in structures: - - # get list of Shapely shapes that intersect at the plane - shapes_plane = plane.intersections_with(structure.geometry) - - # Append each of them and their medium information to the list of shapes - for shape in shapes_plane: - shapes.append((structure.medium, shape, shape.bounds)) - - background_shapes = [] - for medium, shape, bounds in shapes: - - minx, miny, maxx, maxy = bounds - - # loop through background_shapes (note: all background are non-intersecting or merged) - for index, (_medium, _shape, _bounds) in enumerate(background_shapes): - - _minx, _miny, _maxx, _maxy = _bounds - - # do a bounding box check to see if any intersection to do anything about - if minx > _maxx or _minx > maxx or miny > _maxy or _miny > maxy: - continue - - # look more closely to see if intersected. - if _shape.is_empty or not shape.intersects(_shape): - continue - - diff_shape = _shape - shape - - # different medium, remove intersection from background shape - if medium != _medium and len(diff_shape.bounds) > 0: - background_shapes[index] = (_medium, diff_shape, diff_shape.bounds) - - # same medium, add diff shape to this shape and mark background shape for removal - else: - shape = shape | diff_shape - background_shapes[index] = None - - # after doing this with all background shapes, add this shape to the background - background_shapes.append((medium, shape, shape.bounds)) - - # remove any existing background shapes that have been marked as 'None' - background_shapes = [b for b in background_shapes if b is not None] - - # filter out any remaining None or empty shapes (shapes with area completely removed) - return [(medium, shape) for (medium, shape, _) in background_shapes if shape] - @cached_property def frequency_range(self) -> FreqBound: """Range of frequencies spanning all sources' frequency dependence. @@ -2610,7 +2263,7 @@ def dt(self) -> float: dl_sum_inv_sq = sum(1 / dl**2 for dl in dl_mins) dl_avg = 1 / np.sqrt(dl_sum_inv_sq) # material factor - n_cfl = min(min(mat.n_cfl for mat in self.mediums), 1) + n_cfl = min(min(mat.n_cfl for mat in self.scene.mediums), 1) return n_cfl * self.courant * dl_avg / C_0 @cached_property @@ -2750,6 +2403,10 @@ def complex_fields(self) -> bool: """ if any(isinstance(boundary[0], BlochBoundary) for boundary in self.boundary_spec.to_list): return True + for medium in self.scene.mediums: + if medium.nonlinear_spec is not None: + if any(model.complex_fields for model in medium._nonlinear_models): + return True return False @cached_property @@ -2975,7 +2632,7 @@ def make_eps_data(coords: Coords): """returns epsilon data on grid of points defined by coords""" arrays = (np.array(coords.x), np.array(coords.y), np.array(coords.z)) eps_background = get_eps( - structure=self.background_structure, frequency=freq, coords=coords + structure=self.scene.background_structure, frequency=freq, coords=coords ) shape = tuple(len(array) for array in arrays) eps_array = eps_background * np.ones(shape, dtype=complex) @@ -3040,7 +2697,11 @@ def custom_datasets(self) -> List[Dataset]: datasets_current_source = [ src.current_dataset for src in self.sources if isinstance(src, CustomCurrentSource) ] - datasets_medium = [mat for mat in self.mediums if isinstance(mat, AbstractCustomMedium)] + datasets_medium = [ + mat + for mat in self.scene.mediums + if isinstance(mat, AbstractCustomMedium) or mat.time_modulated + ] datasets_geometry = [] for struct in self.structures: @@ -3060,7 +2721,7 @@ def _volumetric_structures_grid(self, grid: Grid) -> Tuple[Structure]: """Generate a tuple of structures wherein any 2D materials are converted to 3D volumetric equivalents, using ``grid`` as the simulation grid.""" - if not any(isinstance(medium, Medium2D) for medium in self.mediums): + if not any(isinstance(medium, Medium2D) for medium in self.scene.mediums): return self.structures def get_bounds(geom: Geometry, axis: Axis) -> Tuple[float, float]: @@ -3125,7 +2786,7 @@ def get_neighboring_media( geom_shifted = set_bounds( geom, bounds=(center + dl_signed, center + dl_signed), axis=axis ) - media = self.intersecting_media(Box.from_bounds(*geom_shifted.bounds), structures) + media = Scene.intersecting_media(Box.from_bounds(*geom_shifted.bounds), structures) if len(media) > 1: raise SetupError( "2D materials do not support multiple neighboring media on a side. " @@ -3155,7 +2816,11 @@ def get_neighboring_media( # get neighboring media and grid sizes (neighbors, dls) = get_neighboring_media(geometry, axis, background_structures) - new_bounds = (center - dls[0] / 2, center + dls[1] / 2) + if not structure.medium.is_pec: + new_bounds = (center - dls[0] / 2, center + dls[1] / 2) + else: + new_bounds = (center, center) + new_geometry = set_bounds(structure.geometry, bounds=new_bounds, axis=axis) new_medium = structure.medium.volumetric_equivalent( @@ -3175,7 +2840,7 @@ def volumetric_structures(self) -> Tuple[Structure]: def allow_gain(self) -> bool: """``True`` if any of the mediums in the simulation allows gain.""" - for medium in self.mediums: + for medium in self.scene.mediums: if isinstance(medium, AnisotropicMedium): if np.any([med.allow_gain for med in [medium.xx, medium.yy, medium.zz]]): return True @@ -3188,6 +2853,7 @@ def perturbed_mediums_copy( temperature: SpatialDataArray = None, electron_density: SpatialDataArray = None, hole_density: SpatialDataArray = None, + interp_method: InterpMethod = "linear", ) -> Simulation: """Return a copy of the simulation with heat and/or charge data applied to all mediums that have perturbation models specified. That is, such mediums will be replaced with @@ -3203,6 +2869,9 @@ def perturbed_mediums_copy( Electron density field data. hole_density : SpatialDataArray = None Hole density field data. + interp_method : :class:`.InterpMethod`, optional + Interpolation method to obtain heat and/or charge values that are not supplied + at the Yee grids. Returns ------- @@ -3212,7 +2881,7 @@ def perturbed_mediums_copy( sim_dict = self.dict() structures = self.structures - sim_bounds = self.bounds_pml + sim_bounds = self.simulation_bounds array_dict = { "temperature": temperature, "electron_density": electron_density, @@ -3247,7 +2916,7 @@ def perturbed_mediums_copy( f"Provided '{name}' does not fully cover structures[{s_ind}]." ) - new_medium = med.perturbed_copy(**restricted_arrays) + new_medium = med.perturbed_copy(**restricted_arrays, interp_method=interp_method) new_structure = structure.updated_copy(medium=new_medium) new_structures.append(new_structure) else: @@ -3273,6 +2942,45 @@ def perturbed_mediums_copy( if not array.does_cover(bounds): log.warning(f"Provided '{name}' does not fully cover simulation domain.") - sim_dict["medium"] = med.perturbed_copy(**restricted_arrays) + sim_dict["medium"] = med.perturbed_copy( + **restricted_arrays, interp_method=interp_method + ) return Simulation.parse_obj(sim_dict) + + @classmethod + def from_scene(cls, scene: Scene, **kwargs) -> Simulation: + """Create a simulation from a :class:.`Scene` instance. Must provide additional parameters + to define a valid simulation (for example, ``run_time``, ``grid_spec``, etc). + + Parameters + ---------- + scene : :class:.`Scene` + Size of object in x, y, and z directions. + **kwargs + Other arguments + + Example + ------- + >>> from tidy3d import Scene, Medium, Box, Structure, GridSpec + >>> box = Structure( + ... geometry=Box(center=(0, 0, 0), size=(1, 2, 3)), + ... medium=Medium(permittivity=5), + ... ) + >>> scene = Scene( + ... structures=[box], + ... medium=Medium(permittivity=3), + ... ) + >>> sim = Simulation.from_scene( + ... scene=scene, + ... center=(0, 0, 0), + ... size=(5, 6, 7), + ... run_time=1e-12, + ... grid_spec=GridSpec.uniform(dl=0.4), + ... ) + """ + return Simulation( + structures=scene.structures, + medium=scene.medium, + **kwargs, + ) diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index e7868fe35..406e9dde8 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -9,12 +9,13 @@ import pydantic.v1 as pydantic import numpy as np -from .base import Tidy3dBaseModel, cached_property - +from .base import cached_property +from .base_sim.source import AbstractSource +from .time import AbstractTimeDependence from .types import Coordinate, Direction, Polarization, Ax, FreqBound from .types import ArrayFloat1D, Axis, PlotVal, ArrayComplex1D, TYPE_TAG_STR -from .validators import assert_plane, assert_volumetric, validate_name_str, get_value -from .validators import warn_if_dataset_none, assert_single_freq_in_range +from .validators import assert_plane, assert_volumetric, get_value +from .validators import warn_if_dataset_none, assert_single_freq_in_range, _assert_min_freq from .data.dataset import FieldDataset, TimeDataset from .data.data_array import TimeDataArray from .geometry.base import Box @@ -26,8 +27,6 @@ from ..exceptions import SetupError, ValidationError from ..log import log -# in spectrum computation, discard amplitudes with relative magnitude smaller than cutoff -DFT_CUTOFF = 1e-8 # when checking if custom data spans the source plane, allow for a small tolerance # due to numerical precision DATA_SPAN_TOL = 1e-8 @@ -37,129 +36,9 @@ WARN_NUM_FREQS = 20 -class SourceTime(ABC, Tidy3dBaseModel): +class SourceTime(AbstractTimeDependence): """Base class describing the time dependence of a source.""" - amplitude: pydantic.NonNegativeFloat = pydantic.Field( - 1.0, title="Amplitude", description="Real-valued maximum amplitude of the time dependence." - ) - - phase: float = pydantic.Field( - 0.0, title="Phase", description="Phase shift of the time dependence.", units=RADIAN - ) - - @abstractmethod - def amp_time(self, time: float) -> complex: - """Complex-valued source amplitude as a function of time. - - Parameters - ---------- - time : float - Time in seconds. - - Returns - ------- - complex - Complex-valued source amplitude at that time.. - """ - - def spectrum( - self, - times: ArrayFloat1D, - freqs: ArrayFloat1D, - dt: float, - complex_fields: bool = False, - ) -> complex: - """Complex-valued source spectrum as a function of frequency - - Parameters - ---------- - times : np.ndarray - Times to use to evaluate spectrum Fourier transform. - (Typically the simulation time mesh). - freqs : np.ndarray - Frequencies in Hz to evaluate spectrum at. - dt : float or np.ndarray - Time step to weight FT integral with. - If array, use to weigh each of the time intervals in ``times``. - complex_fields : bool - Whether time domain fields are complex, e.g., for Bloch boundaries - - Returns - ------- - np.ndarray - Complex-valued array (of len(freqs)) containing spectrum at those frequencies. - """ - - times = np.array(times) - freqs = np.array(freqs) - time_amps = self.amp_time(times) - - if not complex_fields: - time_amps = np.real(time_amps) - - # Cut to only relevant times - relevant_time_inds = np.where(np.abs(time_amps) / np.amax(np.abs(time_amps)) > DFT_CUTOFF) - # find first and last index where the filter is True - start_ind = relevant_time_inds[0][0] - stop_ind = relevant_time_inds[0][-1] - time_amps = time_amps[start_ind:stop_ind] - times_cut = times[start_ind:stop_ind] - - # only need to compute DTFT kernel for distinct dts - # usually, there is only one dt, if times is simulation time mesh - dts = np.diff(times_cut) - dts_unique, kernel_indices = np.unique(dts, return_inverse=True) - - dft_kernels = [np.exp(2j * np.pi * freqs * curr_dt) for curr_dt in dts_unique] - running_kernel = np.exp(2j * np.pi * freqs * times_cut[0]) - dft = np.zeros(len(freqs), dtype=complex) - for amp, kernel_index in zip(time_amps, kernel_indices): - dft += running_kernel * amp - running_kernel *= dft_kernels[kernel_index] - - # kernel_indices was one index shorter than time_amps - dft += running_kernel * time_amps[-1] - - return dt * dft / np.sqrt(2 * np.pi) - - @add_ax_if_none - def plot(self, times: ArrayFloat1D, val: PlotVal = "real", ax: Ax = None) -> Ax: - """Plot the complex-valued amplitude of the source time-dependence. - - Parameters - ---------- - times : np.ndarray - Array of times (seconds) to plot source at. - To see source time amplitude for a specific :class:`Simulation`, - pass ``simulation.tmesh``. - val : Literal['real', 'imag', 'abs'] = 'real' - Which part of the spectrum to plot. - 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) - - if val == "real": - ax.plot(times, amp_complex.real, color="blueviolet", label="real") - elif val == "imag": - ax.plot(times, amp_complex.imag, color="crimson", label="imag") - elif val == "abs": - ax.plot(times, np.abs(amp_complex), color="k", label="abs") - else: - raise ValueError(f"Plot 'val' option of '{val}' not recognized.") - ax.set_xlabel("time (s)") - ax.set_title("source amplitude") - ax.legend() - ax.set_aspect("auto") - return ax - @add_ax_if_none def plot_spectrum( self, @@ -167,9 +46,9 @@ def plot_spectrum( num_freqs: int = 101, val: PlotVal = "real", ax: Ax = None, - complex_fields: bool = False, ) -> Ax: """Plot the complex-valued amplitude of the source time-dependence. + Note: Only the real part of the time signal is used. Parameters ---------- @@ -182,40 +61,17 @@ def plot_spectrum( Number of frequencies to plot within the SourceTime.frequency_range. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. - complex_fields : bool - Whether time domain fields are complex, e.g., for Bloch boundaries Returns ------- matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - times = np.array(times) - - dts = np.diff(times) - if not np.allclose(dts, dts[0] * np.ones_like(dts), atol=1e-17): - raise SetupError("Supplied times not evenly spaced.") - - dt = np.mean(dts) fmin, fmax = self.frequency_range() - freqs = np.linspace(fmin, fmax, num_freqs) - - spectrum = self.spectrum(times=times, dt=dt, freqs=freqs, complex_fields=complex_fields) - - if val == "real": - ax.plot(freqs, spectrum.real, color="blueviolet", label="real") - elif val == "imag": - ax.plot(freqs, spectrum.imag, color="crimson", label="imag") - elif val == "abs": - ax.plot(freqs, np.abs(spectrum), color="k", label="abs") - else: - raise ValueError(f"Plot 'val' option of '{val}' not recognized.") - ax.set_xlabel("frequency (Hz)") - ax.set_title("source spectrum") - ax.legend() - ax.set_aspect("auto") - return ax + return self.plot_spectrum_in_frequency_range( + times, fmin, fmax, num_freqs=num_freqs, val=val, ax=ax + ) @abstractmethod def frequency_range(self, num_fwidth: float = 4.0) -> FreqBound: @@ -310,6 +166,11 @@ class ContinuousWave(Pulse): """Source time dependence that ramps up to continuous oscillation and holds until end of simulation. + Note + ---- + Field decay will not occur, so the simulation will run for the full ``run_time``. + Also, source normalization of frequency-domain monitors is not meaningful. + Example ------- >>> cw = ContinuousWave(freq0=200e12, fwidth=20e12) @@ -342,6 +203,13 @@ class CustomSourceTime(Pulse): e^{i \\cdot phase - 2 \\pi i \\cdot freq0 \\cdot t} \\cdot \\ envelope(t - offset / (2 \\pi \\cdot fwidth)) + Note + ---- + Depending on the envelope, field decay may not occur. + If field decay does not occur, then the simulation will run for the full ``run_time``. + Also, if field decay does not occur, then source normalization of frequency-domain + monitors is not meaningful. + Note ---- The source time dependence is linearly interpolated to the simulation time steps. @@ -466,7 +334,7 @@ def amp_time(self, time: float) -> complex: """ Source objects """ -class Source(Box, ABC): +class Source(Box, AbstractSource, ABC): """Abstract base class for all sources.""" source_time: SourceTimeType = pydantic.Field( @@ -476,15 +344,11 @@ class Source(Box, ABC): discriminator=TYPE_TAG_STR, ) - name: str = pydantic.Field(None, title="Name", description="Optional name for the source.") - @cached_property def plot_params(self) -> PlotParams: """Default parameters for plotting a Source object.""" return plot_params_source - _name_validator = validate_name_str() - @cached_property def geometry(self) -> Box: """:class:`Box` representation of source.""" @@ -506,6 +370,12 @@ def _pol_vector(self) -> Tuple[float, float, float]: """Returns a vector indicating the source polarization for arrow plotting, if not None.""" return None + @pydantic.validator("source_time", always=True) + def _freqs_lower_bound(cls, val): + """Raise validation error if central frequency is too low.""" + _assert_min_freq(val.freq0, msg_start="'source_time.freq0'") + return val + def plot( # pylint:disable=too-many-arguments self, x: float = None, diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index accbdfaea..6815db054 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -1,16 +1,29 @@ """Defines Geometric objects with Medium properties.""" from typing import Union, Tuple, Optional import pydantic.v1 as pydantic +import numpy as np from .base import Tidy3dBaseModel from .validators import validate_name_str -from .geometry.utils import GeometryType +from .geometry.utils import GeometryType, validate_no_transformed_polyslabs from .medium import MediumType, AbstractCustomMedium, Medium2D from .types import Ax, TYPE_TAG_STR, Axis from .viz import add_ax_if_none, equal_aspect from .grid.grid import Coords from ..constants import MICROMETER -from ..exceptions import SetupError +from ..exceptions import SetupError, Tidy3dError, Tidy3dImportError + +try: + gdstk_available = True + import gdstk +except ImportError: + gdstk_available = False + +try: + gdspy_available = True + import gdspy +except ImportError: + gdspy_available = False class AbstractStructure(Tidy3dBaseModel): @@ -29,6 +42,12 @@ class AbstractStructure(Tidy3dBaseModel): _name_validator = validate_name_str() + @pydantic.validator("geometry") + def _transformed_slanted_polyslabs_not_allowed(cls, val): + """Prevents the creation of slanted polyslabs rotated out of plane.""" + validate_no_transformed_polyslabs(val) + return val + @equal_aspect @add_ax_if_none def plot( @@ -138,6 +157,233 @@ def eps_comp(self, row: Axis, col: Axis, frequency: float, coords: Coords) -> co ) return self.medium.eps_comp(row=row, col=col, frequency=frequency) + def to_gdstk( + self, + x: float = None, + y: float = None, + z: float = None, + permittivity_threshold: pydantic.NonNegativeFloat = 1, + frequency: pydantic.PositiveFloat = 0, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + ) -> None: + """Convert a structure's planar slice to a .gds type polygon. + + 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. + permittivity_threshold : float = 1.1 + Permitivitty value used to define the shape boundaries for structures with custom + medim + frequency : float = 0 + Frequency for permittivity evaluaiton in case of custom medium (Hz). + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + + Return + ------ + List + List of `gdstk.Polygon` + """ + + polygons = self.geometry.to_gdstk(x=x, y=y, z=z, gds_layer=gds_layer, gds_dtype=gds_dtype) + + if isinstance(self.medium, AbstractCustomMedium): + axis, _ = self.geometry.parse_xyz_kwargs(x=x, y=y, z=z) + bb_min, bb_max = self.geometry.bounds + + # Set the contour scale to be the minimal cooridante step size w.r.t. the 3 main axes, + # skipping those with a single coordniate. In case all axes have only a single coordinate, + # use the largest bounding box dimension. + eps, _, _ = self.medium.eps_dataarray_freq(frequency=frequency) + scale = max(b - a for a, b in zip(bb_min, bb_max)) + for coord in (eps.x, eps.y, eps.z): + if len(coord) > 1: + scale = min(scale, np.diff(coord).min()) + + coords = Coords( + x=np.arange(bb_min[0], bb_max[0] + scale * 0.9, scale) if x is None else x, + y=np.arange(bb_min[1], bb_max[1] + scale * 0.9, scale) if y is None else y, + z=np.arange(bb_min[2], bb_max[2] + scale * 0.9, scale) if z is None else z, + ) + eps = self.medium.eps_diagonal_on_grid(frequency=frequency, coords=coords) + eps = np.stack((eps[0].real, eps[1].real, eps[2].real), axis=3).max(axis=3).squeeze() + contours = gdstk.contour(eps.T, permittivity_threshold, scale, precision=scale * 1e-3) + + _, (dx, dy) = self.geometry.pop_axis(bb_min, axis) + for polygon in contours: + polygon.translate(dx, dy) + + polygons = gdstk.boolean(polygons, contours, "and", layer=gds_layer, datatype=gds_dtype) + + return polygons + + def to_gdspy( + self, + x: float = None, + y: float = None, + z: float = None, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + ) -> None: + """Convert a structure's planar slice to a .gds type polygon. + + 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. + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + + Return + ------ + List + List of `gdspy.Polygon` and `gdspy.PolygonSet`. + """ + + if isinstance(self.medium, AbstractCustomMedium): + raise Tidy3dError( + "Structures with custom medium are not supported by 'gdspy'. They can only be " + "exported using 'to_gdstk'." + ) + + return self.geometry.to_gdspy(x=x, y=y, z=z, gds_layer=gds_layer, gds_dtype=gds_dtype) + + def to_gds( + self, + cell, + x: float = None, + y: float = None, + z: float = None, + permittivity_threshold: pydantic.NonNegativeFloat = 1, + frequency: pydantic.PositiveFloat = 0, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + ) -> None: + """Append a structure's planar slice to a .gds cell. + + Parameters + ---------- + cell : ``gdstk.Cell`` or ``gdspy.Cell`` + Cell object to which the generated polygons are added. + 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. + permittivity_threshold : float = 1.1 + Permitivitty value used to define the shape boundaries for structures with custom + medim + frequency : float = 0 + Frequency for permittivity evaluaiton in case of custom medium (Hz). + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + """ + if gdstk_available and isinstance(cell, gdstk.Cell): + polygons = self.to_gdstk( + x=x, + y=y, + z=z, + permittivity_threshold=permittivity_threshold, + frequency=frequency, + gds_layer=gds_layer, + gds_dtype=gds_dtype, + ) + if len(polygons) > 0: + cell.add(*polygons) + + elif gdspy_available and isinstance(cell, gdspy.Cell): + polygons = self.to_gdspy(x=x, y=y, z=z, gds_layer=gds_layer, gds_dtype=gds_dtype) + if len(polygons) > 0: + cell.add(polygons) + + elif "gdstk" in cell.__class__ and not gdstk_available: + raise Tidy3dImportError( + "Module 'gdstk' not found. It is required to export shapes to gdstk cells." + ) + elif "gdspy" in cell.__class__ and not gdspy_available: + raise Tidy3dImportError( + "Module 'gdspy' not found. It is required to export shapes to gdspy cells." + ) + else: + raise Tidy3dError( + "Argument 'cell' must be an instance of 'gdstk.Cell' or 'gdspy.Cell'." + ) + + def to_gds_file( + self, + fname: str, + x: float = None, + y: float = None, + z: float = None, + permittivity_threshold: pydantic.NonNegativeFloat = 1, + frequency: pydantic.PositiveFloat = 0, + gds_layer: pydantic.NonNegativeInt = 0, + gds_dtype: pydantic.NonNegativeInt = 0, + gds_cell_name: str = "MAIN", + ) -> None: + """Export a structure's planar slice to a .gds file. + + Parameters + ---------- + fname : str + Full path to the .gds file to save the :class:`Structure` slice to. + 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. + permittivity_threshold : float = 1.1 + Permitivitty value used to define the shape boundaries for structures with custom + medim + frequency : float = 0 + Frequency for permittivity evaluaiton in case of custom medium (Hz). + gds_layer : int = 0 + Layer index to use for the shapes stored in the .gds file. + gds_dtype : int = 0 + Data-type index to use for the shapes stored in the .gds file. + gds_cell_name : str = 'MAIN' + Name of the cell created in the .gds file to store the geometry. + """ + if gdstk_available: + library = gdstk.Library() + elif gdspy_available: + library = gdspy.GdsLibrary() + else: + raise Tidy3dImportError( + "Python modules 'gdspy' and 'gdstk' not found. To export geometries to .gds " + "files, please install one of those those modules." + ) + cell = library.new_cell(gds_cell_name) + self.to_gds( + cell, + x=x, + y=y, + z=z, + permittivity_threshold=permittivity_threshold, + frequency=frequency, + gds_layer=gds_layer, + gds_dtype=gds_dtype, + ) + library.write_gds(fname) + class MeshOverrideStructure(AbstractStructure): """Defines an object that is only used in the process of generating the mesh. diff --git a/tidy3d/components/time.py b/tidy3d/components/time.py new file mode 100644 index 000000000..1117ec6dd --- /dev/null +++ b/tidy3d/components/time.py @@ -0,0 +1,201 @@ +""" Defines time dependence """ +from __future__ import annotations +from abc import ABC, abstractmethod + +import pydantic.v1 as pydantic +import numpy as np + +from .base import Tidy3dBaseModel + +from .types import Ax +from .types import ArrayFloat1D, PlotVal +from .viz import add_ax_if_none +from ..constants import RADIAN +from ..exceptions import SetupError + +# in spectrum computation, discard amplitudes with relative magnitude smaller than cutoff +DFT_CUTOFF = 1e-8 + + +class AbstractTimeDependence(ABC, Tidy3dBaseModel): + """Base class describing time dependence.""" + + amplitude: pydantic.NonNegativeFloat = pydantic.Field( + 1.0, title="Amplitude", description="Real-valued maximum amplitude of the time dependence." + ) + + phase: float = pydantic.Field( + 0.0, title="Phase", description="Phase shift of the time dependence.", units=RADIAN + ) + + @abstractmethod + def amp_time(self, time: float) -> complex: + """Complex-valued amplitude as a function of time. + + Parameters + ---------- + time : float + Time in seconds. + + Returns + ------- + complex + Complex-valued amplitude at that time. + """ + + def spectrum( + self, + times: ArrayFloat1D, + freqs: ArrayFloat1D, + dt: float, + ) -> complex: + """Complex-valued spectrum as a function of frequency. + Note: Only the real part of the time signal is used. + + Parameters + ---------- + times : np.ndarray + Times to use to evaluate spectrum Fourier transform. + (Typically the simulation time mesh). + freqs : np.ndarray + Frequencies in Hz to evaluate spectrum at. + dt : float or np.ndarray + Time step to weight FT integral with. + If array, use to weigh each of the time intervals in ``times``. + + Returns + ------- + np.ndarray + Complex-valued array (of len(freqs)) containing spectrum at those frequencies. + """ + + times = np.array(times) + freqs = np.array(freqs) + time_amps = np.real(self.amp_time(times)) + + # if all time amplitudes are zero, just return (complex-valued) zeros for spectrum + if np.all(np.equal(time_amps, 0.0)): + return (0.0 + 0.0j) * np.zeros_like(freqs) + + # Cut to only relevant times + relevant_time_inds = np.where(np.abs(time_amps) / np.amax(np.abs(time_amps)) > DFT_CUTOFF) + # find first and last index where the filter is True + start_ind = relevant_time_inds[0][0] + stop_ind = relevant_time_inds[0][-1] + 1 + time_amps = time_amps[start_ind:stop_ind] + times_cut = times[start_ind:stop_ind] + if times_cut.size == 0: + return (0.0 + 0.0j) * np.zeros_like(freqs) + + # only need to compute DTFT kernel for distinct dts + # usually, there is only one dt, if times is simulation time mesh + dts = np.diff(times_cut) + dts_unique, kernel_indices = np.unique(dts, return_inverse=True) + + dft_kernels = [np.exp(2j * np.pi * freqs * curr_dt) for curr_dt in dts_unique] + running_kernel = np.exp(2j * np.pi * freqs * times_cut[0]) + dft = np.zeros(len(freqs), dtype=complex) + for amp, kernel_index in zip(time_amps, kernel_indices): + dft += running_kernel * amp + running_kernel *= dft_kernels[kernel_index] + + # kernel_indices was one index shorter than time_amps + dft += running_kernel * time_amps[-1] + + return dt * dft / np.sqrt(2 * np.pi) + + @add_ax_if_none + def plot_spectrum_in_frequency_range( + self, + times: ArrayFloat1D, + fmin: float, + fmax: float, + num_freqs: int = 101, + val: PlotVal = "real", + ax: Ax = None, + ) -> Ax: + """Plot the complex-valued amplitude of the time-dependence. + Note: Only the real part of the time signal is used. + + Parameters + ---------- + times : np.ndarray + Array of evenly-spaced times (seconds) to evaluate time-dependence at. + The spectrum is computed from this value and the time frequency content. + To see spectrum for a specific :class:`Simulation`, + pass ``simulation.tmesh``. + fmin : float + Lower bound of frequency for the spectrum plot. + fmax : float + Upper bound of frequency for the spectrum plot. + num_freqs : int = 101 + Number of frequencies to plot within the [fmin, fmax]. + 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) + + dts = np.diff(times) + if not np.allclose(dts, dts[0] * np.ones_like(dts), atol=1e-17): + raise SetupError("Supplied times not evenly spaced.") + + dt = np.mean(dts) + freqs = np.linspace(fmin, fmax, num_freqs) + + spectrum = self.spectrum(times=times, dt=dt, freqs=freqs) + + if val == "real": + ax.plot(freqs, spectrum.real, color="blueviolet", label="real") + elif val == "imag": + ax.plot(freqs, spectrum.imag, color="crimson", label="imag") + elif val == "abs": + ax.plot(freqs, np.abs(spectrum), color="k", label="abs") + else: + raise ValueError(f"Plot 'val' option of '{val}' not recognized.") + ax.set_xlabel("frequency (Hz)") + ax.set_title("source spectrum") + ax.legend() + ax.set_aspect("auto") + return ax + + @add_ax_if_none + def plot(self, times: ArrayFloat1D, val: PlotVal = "real", ax: Ax = None) -> Ax: + """Plot the complex-valued amplitude of the time-dependence. + + Parameters + ---------- + times : np.ndarray + Array of times (seconds) to plot source at. + To see source time amplitude for a specific :class:`Simulation`, + pass ``simulation.tmesh``. + val : Literal['real', 'imag', 'abs'] = 'real' + Which part of the spectrum to plot. + 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) + + if val == "real": + ax.plot(times, amp_complex.real, color="blueviolet", label="real") + elif val == "imag": + ax.plot(times, amp_complex.imag, color="crimson", label="imag") + elif val == "abs": + ax.plot(times, np.abs(amp_complex), color="k", label="abs") + else: + raise ValueError(f"Plot 'val' option of '{val}' not recognized.") + ax.set_xlabel("time (s)") + ax.set_title("source amplitude") + ax.legend() + ax.set_aspect("auto") + return ax diff --git a/tidy3d/components/time_modulation.py b/tidy3d/components/time_modulation.py new file mode 100644 index 000000000..3b2aa29fc --- /dev/null +++ b/tidy3d/components/time_modulation.py @@ -0,0 +1,247 @@ +""" Defines time modulation to the medium""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import Union +from math import isclose + +import pydantic.v1 as pd +import numpy as np + +from .base import Tidy3dBaseModel, cached_property +from .types import InterpMethod +from .time import AbstractTimeDependence +from .data.data_array import SpatialDataArray +from ..exceptions import ValidationError +from ..constants import HERTZ, RADIAN + + +class AbstractTimeModulation(AbstractTimeDependence, ABC): + """Base class for modulation in time. + + Note + ---- + This class describes the time dependence part of the separable space-time modulation type + as shown below, + + .. math:: + + amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] + + """ + + @cached_property + @abstractmethod + def max_modulation(self) -> float: + """Estimated maximum modulation amplitude.""" + + +class ContinuousWaveTimeModulation(AbstractTimeDependence): + """Class describing modulation with a harmonic time dependence. + + Note + ---- + .. math:: + + amp\\_time(t) = amplitude \\cdot \\ + e^{i \\cdot phase - 2 \\pi i \\cdot freq0 \\cdot t} + + Note + ---- + The full space-time modulation is, + + .. math:: + + amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] + + + Example + ------- + >>> cw = ContinuousWaveTimeModulation(freq0=200e12, amplitude=1, phase=0) + """ + + freq0: pd.PositiveFloat = pd.Field( + ..., title="Modulation Frequency", description="Modulation frequency.", units=HERTZ + ) + + def amp_time(self, time: float) -> complex: + """Complex-valued source amplitude as a function of time.""" + + omega = 2 * np.pi * self.freq0 + return self.amplitude * np.exp(-1j * omega * time + 1j * self.phase) + + @cached_property + def max_modulation(self) -> float: + """Estimated maximum modulation amplitude.""" + return abs(self.amplitude) + + +TimeModulationType = Union[ContinuousWaveTimeModulation] + + +class AbstractSpaceModulation(ABC, Tidy3dBaseModel): + """Base class for modulation in space. + + Note + ---- + This class describes the 2nd term in the full space-time modulation below, + .. math:: + + amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] + + """ + + @cached_property + @abstractmethod + def max_modulation(self) -> float: + """Estimated maximum modulation amplitude.""" + + +class SpaceModulation(AbstractSpaceModulation): + """The modulation profile with a user-supplied spatial distribution of + amplitude and phase. + + Note + ---- + .. math:: + + amp\\_space(r) = amplitude(r) \\cdot e^{i \\cdot phase(r)} + + The full space-time modulation is, + + .. math:: + + amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] + + Example + ------- + >>> Nx, Ny, Nz = 10, 9, 8 + >>> X = np.linspace(-1, 1, Nx) + >>> Y = np.linspace(-1, 1, Ny) + >>> Z = np.linspace(-1, 1, Nz) + >>> coords = dict(x=X, y=Y, z=Z) + >>> amp = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords) + >>> phase = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords) + >>> space = SpaceModulation(amplitude=amp, phase=phase) + """ + + amplitude: Union[float, SpatialDataArray] = pd.Field( + 1, + title="Amplitude of modulation in space", + description="Amplitude of modulation that can vary spatially. " + "It takes the unit of whatever is being modulated.", + ) + + phase: Union[float, SpatialDataArray] = pd.Field( + 0, + title="Phase of modulation in space", + description="Phase of modulation that can vary spatially.", + units=RADIAN, + ) + + interp_method: InterpMethod = pd.Field( + "nearest", + title="Interpolation method", + description="Method of interpolation to use to obtain values at spatial locations on the Yee grids.", + ) + + @pd.validator("amplitude", always=True) + def _real_amplitude(cls, val): + """Assert that the amplitude is real.""" + if np.iscomplexobj(val): + raise ValidationError("'amplitude' must be real.") + return val + + @pd.validator("phase", always=True) + def _real_phase(cls, val): + """Assert that the phase is real.""" + if np.iscomplexobj(val): + raise ValidationError("'phase' must be real.") + return val + + @cached_property + def max_modulation(self) -> float: + """Estimated maximum modulation amplitude.""" + return np.max(abs(np.array(self.amplitude))) + + +SpaceModulationType = Union[SpaceModulation] + + +class SpaceTimeModulation(Tidy3dBaseModel): + """Space-time modulation applied to a medium, adding + on top of the time-independent part. + + + Note + ---- + The space-time modulation must be separable in space and time. + e.g. when applied to permittivity, + + .. math:: + + \\delta \\epsilon(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] + """ + + space_modulation: SpaceModulationType = pd.Field( + SpaceModulation(), + title="Space modulation", + description="Space modulation part from the separable SpaceTimeModulation.", + # discriminator=TYPE_TAG_STR, + ) + + time_modulation: TimeModulationType = pd.Field( + ..., + title="Time modulation", + description="Time modulation part from the separable SpaceTimeModulation.", + # discriminator=TYPE_TAG_STR, + ) + + @cached_property + def max_modulation(self) -> float: + """Estimated maximum modulation amplitude.""" + return self.time_modulation.max_modulation * self.space_modulation.max_modulation + + @cached_property + def negligible_modulation(self) -> bool: + """whether the modulation is weak enough to be regarded as zero.""" + # if isclose(np.diff(time_modulation.range), 0) and + if isclose(self.max_modulation, 0): + return True + return False + + +class ModulationSpec(Tidy3dBaseModel): + """Specification adding space-time modulation to the non-dispersive part of medium + including relative permittivity at infinite frequency and electric conductivity. + """ + + permittivity: SpaceTimeModulation = pd.Field( + None, + title="Space-time modulation of relative permittivity", + description="Space-time modulation of relative permittivity at infinite frequency " + "applied on top of the base permittivity at infinite frequency.", + ) + + conductivity: SpaceTimeModulation = pd.Field( + None, + title="Space-time modulation of conductivity", + description="Space-time modulation of electric conductivity " + "applied on top of the base conductivity.", + ) + + @pd.validator("conductivity", always=True) + def _same_modulation_frequency(cls, val, values): + """Assert same time-modulation applied to permittivity and conductivity.""" + permittivity = values.get("permittivity") + if val is not None and permittivity is not None: + if val.time_modulation != permittivity.time_modulation: + raise ValidationError( + "'permittivity' and 'conductivity' should have the same time modulation." + ) + return val + + @cached_property + def applied_modulation(self) -> bool: + """Check if any modulation has been applied to `permittivity` or `conductivity`.""" + return self.permittivity is not None or self.conductivity is not None diff --git a/tidy3d/components/transformation.py b/tidy3d/components/transformation.py index 6330b477b..012ade3e2 100644 --- a/tidy3d/components/transformation.py +++ b/tidy3d/components/transformation.py @@ -8,8 +8,9 @@ import numpy as np from .base import Tidy3dBaseModel, cached_property -from .types import Coordinate, TensorReal, ArrayFloat2D +from .types import Coordinate, TensorReal, ArrayFloat2D, Axis from ..constants import RADIAN +from ..exceptions import ValidationError class AbstractRotation(ABC, Tidy3dBaseModel): @@ -70,10 +71,11 @@ def rotate_tensor(self, tensor: TensorReal) -> TensorReal: class RotationAroundAxis(AbstractRotation): """Rotation of vectors and tensors around a given vector.""" - axis: Coordinate = pd.Field( - [1, 0, 0], + axis: Union[Axis, Coordinate] = pd.Field( + 0, title="Axis of Rotation", - description="A vector that specifies the axis of rotation.", + description="A vector that specifies the axis of rotation, or a single int: 0, 1, or 2, " + "indicating x, y, or z.", ) angle: float = pd.Field( @@ -83,6 +85,23 @@ class RotationAroundAxis(AbstractRotation): units=RADIAN, ) + @pd.validator("axis", always=True) + def _convert_axis_index_to_vector(cls, val): + if not isinstance(val, tuple): + axis = [0.0, 0.0, 0.0] + axis[val] = 1.0 + val = tuple(axis) + return val + + @pd.validator("axis") + def _guarantee_nonzero_axis(cls, val): + norm = np.linalg.norm(val) + if np.isclose(norm, 0): + raise ValidationError( + "The norm of vector 'axis' cannot be zero. Please provide a proper rotation axis." + ) + return val + @cached_property def isidentity(self) -> bool: """Check whether rotation is identity.""" @@ -96,7 +115,8 @@ def matrix(self) -> TensorReal: if self.isidentity: return np.eye(3) - n = self.axis / np.linalg.norm(self.axis) + norm = np.linalg.norm(self.axis) + n = self.axis / norm c = np.cos(self.angle) s = np.sin(self.angle) R = np.zeros((3, 3)) diff --git a/tidy3d/components/types.py b/tidy3d/components/types.py index 233d3ca4d..a56668869 100644 --- a/tidy3d/components/types.py +++ b/tidy3d/components/types.py @@ -1,6 +1,7 @@ """ Defines 'types' that various fields can be """ -from typing import Tuple, Union +from typing import Tuple, Union, Any +import functools # Literal only available in python 3.8 + so try import otherwise use extensions try: @@ -13,7 +14,56 @@ import numpy as np from matplotlib.axes import Axes from shapely.geometry.base import BaseGeometry -from ..exceptions import ValidationError +from ..exceptions import ValidationError, Tidy3dImportError + + +try: + import trimesh +except ImportError: + trimesh = None + +vtk = { + "mod": None, + "id_type": np.int64, + "vtk_to_numpy": None, + "numpy_to_vtkIdTypeArray": None, + "numpy_to_vtk": None, +} + + +def requires_vtk(fn): + """When decorating a method, requires that vtk is available.""" + + @functools.wraps(fn) + def _fn(*args, **kwargs): + if vtk["mod"] is None: + try: + import vtk as vtk_mod + from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk + from vtk.util.numpy_support import numpy_to_vtkIdTypeArray + from vtkmodules.vtkCommonCore import vtkLogger + + vtk["mod"] = vtk_mod + vtk["vtk_to_numpy"] = vtk_to_numpy + vtk["numpy_to_vtkIdTypeArray"] = numpy_to_vtkIdTypeArray + vtk["numpy_to_vtk"] = numpy_to_vtk + + vtkLogger.SetStderrVerbosity(vtkLogger.VERBOSITY_WARNING) + + if vtk["mod"].vtkIdTypeArray().GetDataTypeSize() == 4: + vtk["id_type"] = np.int32 + + except ImportError: + raise Tidy3dImportError( + "The package 'vtk' is required for this operation, but it was not found. " + "Please install the 'vtk' dependencies using, for example, " + "'pip install -r requirements/vtk.txt'." + ) + + return fn(*args, **kwargs) + + return _fn + # type tag default name TYPE_TAG_STR = "type" @@ -134,6 +184,7 @@ def constrained_array( ArrayComplex4D = constrained_array(dtype=complex, ndim=4) TensorReal = constrained_array(dtype=float, ndim=2, shape=(3, 3)) +MatrixReal4x4 = constrained_array(dtype=float, ndim=2, shape=(4, 4)) """ Complex Values """ @@ -178,6 +229,7 @@ def __modify_schema__(cls, field_schema): """ symmetry """ Symmetry = Literal[0, -1, 1] +ScalarSymmetry = Literal[0, 1] """ geometric """ @@ -192,6 +244,8 @@ def __modify_schema__(cls, field_schema): Shapely = BaseGeometry PlanePosition = Literal["bottom", "middle", "top"] ClipOperationType = Literal["union", "intersection", "difference", "symmetric_difference"] +BoxSurface = Literal["x-", "x+", "y-", "y+", "z-", "z+"] +TrimeshType = Any if trimesh is None else trimesh.Trimesh """ medium """ @@ -224,6 +278,7 @@ def __modify_schema__(cls, field_schema): Ax = Axes PlotVal = Literal["real", "imag", "abs"] FieldVal = Literal["real", "imag", "abs", "abs^2", "phase"] +RealFieldVal = Literal["real", "abs", "abs^2"] PlotScale = Literal["lin", "dB"] ColormapType = Literal["divergent", "sequential", "cyclic"] diff --git a/tidy3d/components/validators.py b/tidy3d/components/validators.py index 9de9f39c7..eb7066f3f 100644 --- a/tidy3d/components/validators.py +++ b/tidy3d/components/validators.py @@ -43,6 +43,9 @@ For more details: `Pydantic Validators `_ """ +# Lowest frequency supported (Hz) +MIN_FREQUENCY = 1e5 + def get_value(key: str, values: dict) -> Any: """Grab value from values dictionary. If not present, raise an error before continuing.""" @@ -330,3 +333,37 @@ def _warn_perturbed_val_range(cls, val, values): return val return _warn_perturbed_val_range + + +def _assert_min_freq(freqs, msg_start: str): + """Check if all ``freqs`` are above the minimum frequency.""" + if np.min(freqs) < MIN_FREQUENCY: + raise ValidationError( + f"{msg_start} must be no lower than {MIN_FREQUENCY:.0e} Hz. " + "Note that the unit of frequency is 'Hz'." + ) + + +def validate_freqs_min(): + """Validate lower bound for monitor, and mode solver frequencies.""" + + @pydantic.validator("freqs", always=True, allow_reuse=True) + def freqs_lower_bound(cls, val): + """Raise validation error if any of ``freqs`` is lower than ``MIN_FREQUENCY``.""" + _assert_min_freq(val, msg_start=f"All of '{cls.__name__}.freqs'") + return val + + return freqs_lower_bound + + +def validate_freqs_not_empty(): + """Validate that the array of frequencies is not empty.""" + + @pydantic.validator("freqs", always=True, allow_reuse=True) + def freqs_not_empty(cls, val): + """Raise validation error if ``freqs`` is an empty Tuple.""" + if len(val) == 0: + raise ValidationError(f"'{cls.__name__}.freqs' cannot be empty (size 0).") + return val + + return freqs_not_empty diff --git a/tidy3d/components/viz.py b/tidy3d/components/viz.py index 89d2bcd55..3f7ea4abd 100644 --- a/tidy3d/components/viz.py +++ b/tidy3d/components/viz.py @@ -114,6 +114,8 @@ def to_kwargs(self) -> dict: plot_params_override_structures = PlotParams( linewidth=0.4, edgecolor="black", fill=False, zorder=inf ) +plot_params_fluid = PlotParams(facecolor="white", edgecolor="lightsteelblue", lw=0.4, hatch="xx") +plot_params_grid = PlotParams(edgecolor="black", lw=0.2) # stores color of simulation.structures for given index in simulation.medium_map MEDIUM_CMAP = [ @@ -129,6 +131,7 @@ def to_kwargs(self) -> dict: # colormap for structure's permittivity in plot_eps STRUCTURE_EPS_CMAP = "gist_yarg" +STRUCTURE_HEAT_COND_CMAP = "gist_yarg" # default arrow style arrow_style = ArrowStyle.Simple(head_length=12, head_width=9, tail_width=4) diff --git a/tidy3d/constants.py b/tidy3d/constants.py index b1716b5da..aeacf056e 100644 --- a/tidy3d/constants.py +++ b/tidy3d/constants.py @@ -47,6 +47,14 @@ KELVIN = "K" CMCUBE = "cm^3" PERCMCUBE = "1/cm^3" +WATT = "W" +VOLT = "V" + +THERMAL_CONDUCTIVITY = "W/(um*K)" +SPECIFIC_HEAT_CAPACITY = "J/(kg*K)" +HEAT_FLUX = "W/um^2" +VOLUMETRIC_HEAT_RATE = "W/um^3" +HEAT_TRANSFER_COEFF = "W/(um^2*K)" # large number used for comparing infinity LARGE_NUMBER = 1e10 diff --git a/tidy3d/log.py b/tidy3d/log.py index 21ade0909..c98e0c10f 100644 --- a/tidy3d/log.py +++ b/tidy3d/log.py @@ -1,6 +1,7 @@ """Logging for Tidy3d.""" import inspect +from datetime import datetime from typing import Union, List from typing_extensions import Literal @@ -317,6 +318,11 @@ def set_log_suppression(value: bool) -> None: log.suppression = value +def get_aware_datetime() -> datetime: + """Get an aware current local datetime(with local timezone info)""" + return datetime.now().astimezone() + + def set_logging_console(stderr: bool = False) -> None: """Set stdout or stderr as console output @@ -330,7 +336,14 @@ def set_logging_console(stderr: bool = False) -> None: else: previous_level = DEFAULT_LEVEL log.handlers["console"] = LogHandler( - Console(stderr=stderr, width=CONSOLE_WIDTH, log_path=False), previous_level + Console( + stderr=stderr, + width=CONSOLE_WIDTH, + log_path=False, + get_datetime=get_aware_datetime, + log_time_format="%X %Z", + ), + previous_level, ) @@ -371,7 +384,7 @@ def set_logging_file( log.error(f"File {fname} could not be opened") return - log.handlers["file"] = LogHandler(Console(file=file), level) + log.handlers["file"] = LogHandler(Console(file=file, force_jupyter=False), level) # Initialize Tidy3d's logger diff --git a/tidy3d/material_library/material_library.py b/tidy3d/material_library/material_library.py index 71c418252..694d1f70e 100644 --- a/tidy3d/material_library/material_library.py +++ b/tidy3d/material_library/material_library.py @@ -2116,14 +2116,14 @@ class MaterialItem2D(MaterialItem): default="Horiba", ), aSi=MaterialItem( - name="Amorphous Silicon", + name="Silicon (Amorphous)", variants=dict( Horiba=aSi_Horiba, ), default="Horiba", ), cSi=MaterialItem( - name="Crystalline Silicon", + name="Silicon (Crystalline)", variants=dict( Palik_Lossless=cSi_PalikLossless, Palik_Lossy=cSi_PalikLossy, diff --git a/tidy3d/plugins/adjoint/__init__.py b/tidy3d/plugins/adjoint/__init__.py index 7630d93f1..9bfc03267 100644 --- a/tidy3d/plugins/adjoint/__init__.py +++ b/tidy3d/plugins/adjoint/__init__.py @@ -4,7 +4,11 @@ try: from .components.geometry import JaxBox, JaxPolySlab, JaxGeometryGroup from .components.medium import JaxMedium, JaxAnisotropicMedium, JaxCustomMedium - from .components.structure import JaxStructure + from .components.structure import ( + JaxStructure, + JaxStructureStaticGeometry, + JaxStructureStaticMedium, + ) from .components.simulation import JaxSimulation from .components.data.sim_data import JaxSimulationData from .components.data.monitor_data import JaxModeData @@ -12,9 +16,9 @@ from .components.data.data_array import JaxDataArray except ImportError as e: raise ImportError( - "The 'jax' package is required for adjoint plugin and not installed. " - "To get the appropriate packages, install tidy3d using '[jax]' option, for example: " - "$pip install 'tidy3d[jax]'." + "The 'jax' package is required for adjoint plugin. We were not able to import it. " + "To get the appropriate packages for your system, install tidy3d using '[jax]' option, " + "for example: $pip install 'tidy3d[jax]'." ) from e try: @@ -30,6 +34,8 @@ "JaxAnisotropicMedium", "JaxCustomMedium", "JaxStructure", + "JaxStructureStaticMedium", + "JaxStructureStaticGeometry", "JaxSimulation", "JaxSimulationData", "JaxModeData", diff --git a/tidy3d/plugins/adjoint/components/__init__.py b/tidy3d/plugins/adjoint/components/__init__.py index 59d5df0ba..e70157765 100644 --- a/tidy3d/plugins/adjoint/components/__init__.py +++ b/tidy3d/plugins/adjoint/components/__init__.py @@ -1,9 +1,9 @@ """Component imports for adjoint plugin. from tidy3d.plugins.adjoint.components import *""" # import the jax version of tidy3d components -from .geometry import JaxBox # , JaxPolySlab +from .geometry import JaxBox, JaxPolySlab from .medium import JaxMedium, JaxAnisotropicMedium, JaxCustomMedium -from .structure import JaxStructure +from .structure import JaxStructure, JaxStructureStaticMedium, JaxStructureStaticGeometry from .simulation import JaxSimulation from .data.sim_data import JaxSimulationData from .data.monitor_data import JaxModeData @@ -18,6 +18,8 @@ "JaxAnisotropicMedium", "JaxCustomMedium", "JaxStructure", + "JaxStructureStaticMedium", + "JaxStructureStaticGeometry", "JaxSimulation", "JaxSimulationData", "JaxModeData", diff --git a/tidy3d/plugins/adjoint/components/base.py b/tidy3d/plugins/adjoint/components/base.py index 459b49e7a..f45d6bc98 100644 --- a/tidy3d/plugins/adjoint/components/base.py +++ b/tidy3d/plugins/adjoint/components/base.py @@ -9,7 +9,7 @@ from jax.tree_util import tree_flatten as jax_tree_flatten from jax.tree_util import tree_unflatten as jax_tree_unflatten -from ....components.base import Tidy3dBaseModel, cached_property +from ....components.base import Tidy3dBaseModel from .data.data_array import JaxDataArray, JAX_DATA_ARRAY_TAG @@ -93,11 +93,10 @@ def from_tidy3d(cls, tidy3d_obj: Tidy3dBaseModel) -> JaxObject: """ IO """ - @cached_property - def _json_string(self) -> str: + def _json(self, *args, **kwargs) -> str: """Overwritten method to get the json string to store in the files.""" - json_string_og = super()._json_string + json_string_og = super()._json(*args, **kwargs) json_dict = json.loads(json_string_og) def strip_data_array(sub_dict: dict) -> None: diff --git a/tidy3d/plugins/adjoint/components/data/monitor_data.py b/tidy3d/plugins/adjoint/components/data/monitor_data.py index ab1c6bc48..f9cb0491d 100644 --- a/tidy3d/plugins/adjoint/components/data/monitor_data.py +++ b/tidy3d/plugins/adjoint/components/data/monitor_data.py @@ -91,8 +91,6 @@ def to_adjoint_sources(self, fwidth: float) -> List[ModeSource]: k0 = 2 * np.pi * freq / C_0 grad_const = k0 / 4 / ETA_0 src_amp = grad_const * amp - if direction == "-": - src_amp *= -1 src_direction = self.flip_direction(str(direction)) @@ -232,90 +230,93 @@ def time_reversed_copy(self) -> FieldData: def to_adjoint_sources(self, fwidth: float) -> List[CustomFieldSource]: """Converts a :class:`.JaxFieldData` to a list of adjoint :class:`.CustomFieldSource.""" - # parse the frequency from the scalar field data - freqs = [scalar_fld.coords["f"] for _, scalar_fld in self.field_components.items()] - if any(len(fs) != 1 for fs in freqs): - raise AdjointError("FieldData must have only one frequency.") - freqs = [fs[0] for fs in freqs] - if len(set(freqs)) != 1: - raise AdjointError("FieldData must all contain the same frequency.") - freq0 = freqs[0] - - omega0 = 2 * np.pi * freq0 - scaling_factor = 1 / (MU_0 * omega0) - interpolate_source = True + sources = [] - # dipole case if np.allclose(np.array(self.monitor.size), np.zeros(3)): - dipoles = [] for polarization, field_component in self.field_components.items(): + if field_component is None: continue - forward_amp = complex(field_component.as_ndarray) - adj_phase = 3 * np.pi / 2 + np.angle(forward_amp) + for freq0 in field_component.coords["f"]: + + omega0 = 2 * np.pi * freq0 + scaling_factor = 1 / (MU_0 * omega0) + + forward_amp = complex(field_component.sel(f=freq0).values) + + adj_phase = 3 * np.pi / 2 + np.angle(forward_amp) + + adj_amp = scaling_factor * forward_amp + + src_adj = PointDipole( + center=self.monitor.center, + polarization=polarization, + source_time=GaussianPulse( + freq0=freq0, fwidth=fwidth, amplitude=abs(adj_amp), phase=adj_phase + ), + interpolate=interpolate_source, + ) + + sources.append(src_adj) + else: - adj_amp = scaling_factor * forward_amp + # Define source geometry based on coordinates in the data + data_mins = [] + data_maxs = [] - src_adj = PointDipole( - center=self.monitor.center, - polarization=polarization, + def shift_value(coords) -> float: + """How much to shift the geometry by along a dimension (only if > 1D).""" + return 1e-5 if len(coords) > 1 else 0 + + for _, field_component in self.field_components.items(): + coords = field_component.coords + data_mins.append({key: min(val) + shift_value(val) for key, val in coords.items()}) + data_maxs.append({key: max(val) + shift_value(val) for key, val in coords.items()}) + + rmin = [] + rmax = [] + for dim in "xyz": + rmin.append(max(val[dim] for val in data_mins)) + rmax.append(min(val[dim] for val in data_maxs)) + + source_geo = Box.from_bounds(rmin=rmin, rmax=rmax) + + # Define source dataset + # Offset coordinates by source center since local coords are assumed in CustomCurrentSource + + for freq0 in tuple(self.field_components.values())[0].coords["f"]: + + src_field_components = {} + for name, field_component in self.field_components.items(): + field_component = field_component.sel(f=freq0) + forward_amps = field_component.as_ndarray + values = -1j * forward_amps + coords = field_component.coords + for dim, key in enumerate("xyz"): + coords[key] = np.array(coords[key]) - source_geo.center[dim] + coords["f"] = np.array([freq0]) + values = np.expand_dims(values, axis=-1) + if not np.all(values == 0): + src_field_components[name] = ScalarFieldDataArray(values, coords=coords) + + dataset = FieldDataset(**src_field_components) + + custom_source = CustomCurrentSource( + center=source_geo.center, + size=source_geo.size, source_time=GaussianPulse( - freq0=freq0, fwidth=fwidth, amplitude=abs(adj_amp), phase=adj_phase + freq0=freq0, + fwidth=fwidth, ), + current_dataset=dataset, interpolate=interpolate_source, ) - dipoles.append(src_adj) - return dipoles - - # Define source geometry based on coordinates in the data - data_mins = [] - data_maxs = [] - - def shift_value(coords) -> float: - """How much to shift the geometry by along a dimension (only if > 1D).""" - return 1e-5 if len(coords) > 1 else 0 - - for _, field_component in self.field_components.items(): - coords = field_component.coords - data_mins.append({key: min(val) + shift_value(val) for key, val in coords.items()}) - data_maxs.append({key: max(val) + shift_value(val) for key, val in coords.items()}) - - rmin = [] - rmax = [] - for dim in "xyz": - rmin.append(max(val[dim] for val in data_mins)) - rmax.append(min(val[dim] for val in data_maxs)) - - source_geo = Box.from_bounds(rmin=rmin, rmax=rmax) - - # Define source dataset - # Offset coordinates by source center since local coords are assumed in CustomCurrentSource - src_field_components = {} - for name, field_component in self.field_components.items(): - forward_amps = field_component.as_ndarray - values = -1j * forward_amps - coords = field_component.coords - for dim, key in enumerate("xyz"): - coords[key] = np.array(coords[key]) - source_geo.center[dim] - if not np.all(values == 0): - src_field_components[name] = ScalarFieldDataArray(values, coords=coords) - - dataset = FieldDataset(**src_field_components) - custom_source = CustomCurrentSource( - center=source_geo.center, - size=source_geo.size, - source_time=GaussianPulse( - freq0=freq0, - fwidth=fwidth, - ), - current_dataset=dataset, - interpolate=interpolate_source, - ) + sources.append(custom_source) - return [custom_source] + return sources @register_pytree_node_class diff --git a/tidy3d/plugins/adjoint/components/data/sim_data.py b/tidy3d/plugins/adjoint/components/data/sim_data.py index 21cdbae2d..8b384ae9f 100644 --- a/tidy3d/plugins/adjoint/components/data/sim_data.py +++ b/tidy3d/plugins/adjoint/components/data/sim_data.py @@ -4,11 +4,15 @@ from typing import Tuple, Dict, Union, List import pydantic.v1 as pd +import numpy as np +import xarray as xr from jax.tree_util import register_pytree_node_class from .....components.data.monitor_data import MonitorDataType, FieldData, PermittivityData from .....components.data.sim_data import SimulationData +from .....components.source import PointDipole, GaussianPulse +from .....log import log from ..base import JaxObject from ..simulation import JaxSimulation, JaxInfo @@ -157,7 +161,7 @@ def split_fwd_sim_data( return user_sim_data, adjoint_sim_data - def make_adjoint_simulation(self, fwidth: float) -> JaxSimulation: + def make_adjoint_simulation(self, fwidth: float, run_time: float) -> JaxSimulation: """Make an adjoint simulation out of the data provided (generally, the vjp sim data).""" sim_fwd = self.simulation @@ -171,11 +175,46 @@ def make_adjoint_simulation(self, fwidth: float) -> JaxSimulation: for adj_source in mnt_data_vjp.to_adjoint_sources(fwidth=fwidth): adj_srcs.append(adj_source) - update_dict = dict(boundary_spec=bc_adj, sources=adj_srcs, monitors=(), output_monitors=()) + # in this case (no adjoint sources) give it an "empty" source + if not adj_srcs: + log.warning( + "No adjoint sources, making a mock source with amplitude = 0. " + "All gradients will be zero for anything depending on this simulation's data. " + "This comes up when a simulation's data contributes to the value of an objective " + "function but the contribution from each member of the data is 0. " + "If this is intended (eg. if using 'jnp.max()' of several simulation results), " + "please ignore. Otherwise, this can suggest a mistake in your objective function." + ) + + # set a zero-amplitude source + adj_srcs.append( + PointDipole( + center=sim_fwd.center, + polarization="Ez", + source_time=GaussianPulse( + freq0=sim_fwd.freqs_adjoint[0], + fwidth=sim_fwd._fwidth_adjoint, + amplitude=0.0, + ), + ) + ) + + # set a very short run time relative to the fwidth + run_time = 2 / fwidth + + update_dict = dict( + boundary_spec=bc_adj, + sources=adj_srcs, + monitors=(), + output_monitors=(), + run_time=run_time, + normalize_index=None, # normalize later, frequency-by-frequency + ) + update_dict.update( sim_fwd.get_grad_monitors( input_structures=sim_fwd.input_structures, - freq_adjoint=sim_fwd.freq_adjoint, + freqs_adjoint=sim_fwd.freqs_adjoint, include_eps_mnts=False, ) ) @@ -188,3 +227,28 @@ def make_adjoint_simulation(self, fwidth: float) -> JaxSimulation: update_dict.update(dict(grid_spec=grid_spec_adj)) return sim_fwd.updated_copy(**update_dict) + + def normalize_adjoint_fields(self) -> JaxSimulationData: + """Make copy of jax_sim_data with grad_data (fields) normalized by adjoint sources.""" + + grad_data_norm = [] + for field_data in self.grad_data: + field_components_norm = {} + for field_name, field_component in field_data.field_components.items(): + freqs = field_component.coords["f"] + norm_factor_f = np.zeros(len(freqs), dtype=complex) + for i, freq in enumerate(freqs): + freq = float(freq) + for source_index, source in enumerate(self.simulation.sources): + if source.source_time.freq0 == freq and source.source_time.amplitude > 0: + spectrum_fn = self.source_spectrum(source_index) + norm_factor_f[i] = complex(spectrum_fn([freq])[0]) + + norm_factor_f_darr = xr.DataArray(norm_factor_f, coords=dict(f=freqs)) + field_component_norm = field_component / norm_factor_f_darr + field_components_norm[field_name] = field_component_norm + + field_data_norm = field_data.updated_copy(**field_components_norm) + grad_data_norm.append(field_data_norm) + + return self.updated_copy(grad_data=grad_data_norm) diff --git a/tidy3d/plugins/adjoint/components/geometry.py b/tidy3d/plugins/adjoint/components/geometry.py index 0bff9607c..78a061657 100644 --- a/tidy3d/plugins/adjoint/components/geometry.py +++ b/tidy3d/plugins/adjoint/components/geometry.py @@ -2,13 +2,12 @@ from __future__ import annotations from abc import ABC -from typing import Tuple, Union, Dict +from typing import Tuple, Union, Dict, List from multiprocessing import Pool import pydantic.v1 as pd import numpy as np import xarray as xr -import jax.numpy as jnp from jax.tree_util import register_pytree_node_class import jax @@ -69,7 +68,7 @@ def bounding_box(self): return JaxBox.from_bounds(*self.bounds) def make_grad_monitors( - self, freq: float, name: str + self, freqs: List[float], name: str ) -> Tuple[FieldMonitor, PermittivityMonitor]: """Return gradient monitor associated with this object.""" size_enlarged = tuple(s + 2 * GRAD_MONITOR_EXPANSION for s in self.bound_size) @@ -77,7 +76,7 @@ def make_grad_monitors( size=size_enlarged, center=self.bound_center, fields=["Ex", "Ey", "Ez"], - freqs=[freq], + freqs=freqs, name=name + "_field", colocate=False, ) @@ -85,7 +84,7 @@ def make_grad_monitors( eps_mnt = PermittivityMonitor( size=size_enlarged, center=self.bound_center, - freqs=[freq], + freqs=freqs, name=name + "_eps", ) return field_mnt, eps_mnt @@ -234,7 +233,7 @@ def store_vjp( # select the permittivity data eps_field_name = f"eps_{field_cmp_dim}{field_cmp_dim}" - eps_data = grad_data_eps.field_components[eps_field_name].isel(f=0) + eps_data = grad_data_eps.field_components[eps_field_name] # get the permittivity values just inside and outside the edge @@ -265,7 +264,7 @@ def store_vjp( delta_eps_inv = 1.0 / eps1 - 1.0 / eps2 d_integrand = -(delta_eps_inv * d_normal).real d_integrand = d_integrand.interp(**area_coords, assume_sorted=True) - grad_contrib = d_area * jnp.sum(d_integrand.values) + grad_contrib = d_area * np.sum(d_integrand.values) # get gradient contribution for parallel components using parallel E fields else: @@ -278,14 +277,14 @@ def store_vjp( delta_eps = eps1 - eps2 e_integrand = +(delta_eps * e_parallel).real e_integrand = e_integrand.interp(**area_coords, assume_sorted=True) - grad_contrib = d_area * jnp.sum(e_integrand.values) + grad_contrib = d_area * np.sum(e_integrand.values) # add this field contribution to the dict storing the surface contributions vjp_surfs[dim_normal][min_max_index] += grad_contrib - # convert surface vjps to center, size vjps. Note, convert these to jax types w/ jnp.sum() - vjp_center = tuple(jnp.sum(vjp_surfs[dim][1] - vjp_surfs[dim][0]) for dim in "xyz") - vjp_size = tuple(jnp.sum(0.5 * (vjp_surfs[dim][1] + vjp_surfs[dim][0])) for dim in "xyz") + # convert surface vjps to center, size vjps. Note, convert these to jax types w/ np.sum() + vjp_center = tuple(np.sum(vjp_surfs[dim][1] - vjp_surfs[dim][0]) for dim in "xyz") + vjp_size = tuple(np.sum(0.5 * (vjp_surfs[dim][1] + vjp_surfs[dim][0])) for dim in "xyz") return self.copy(update=dict(center=vjp_center, size=vjp_size)) @@ -448,7 +447,6 @@ def compute_integrand(s: np.array, z: np.array) -> np.array: def evaluate(scalar_field: ScalarFieldDataArray) -> float: """Evaluate a scalar field at a coordinate along the edge.""" - scalar_field = scalar_field.isel(f=0) # if only 1 z coordinate, just isel the data. if len(z) == 1: @@ -506,7 +504,7 @@ def evaluate(scalar_field: ScalarFieldDataArray) -> float: dz = 1.0 # integrate by summing over axis edge (z) and parameterization point (s) - integrand = compute_integrand(s=s_vals, z=z_vals) + integrand = compute_integrand(s=s_vals, z=z_vals).sum(dim="f") integral_result = np.sum(integrand.fillna(0).values) # project to the normal direction diff --git a/tidy3d/plugins/adjoint/components/medium.py b/tidy3d/plugins/adjoint/components/medium.py index 81659a80d..dc6826e7d 100644 --- a/tidy3d/plugins/adjoint/components/medium.py +++ b/tidy3d/plugins/adjoint/components/medium.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Dict, Tuple, Union, Callable, Optional -from abc import ABC +from abc import ABC, abstractmethod import pydantic.v1 as pd import numpy as np @@ -34,6 +34,14 @@ class AbstractJaxMedium(ABC, JaxObject): """Holds some utility functions for Jax medium types.""" + def to_tidy3d(self) -> AbstractJaxMedium: + """Convert self to tidy3d component.""" + return self.to_medium() + + @abstractmethod + def to_medium(self) -> AbstractJaxMedium: + """Convert self to medium.""" + def _get_volume_disc( self, grad_data: FieldData, sim_bounds: Bound, wvl_mat: float ) -> Tuple[Dict[str, np.ndarray], float]: @@ -191,10 +199,16 @@ def store_vjp( inside_fn=inside_fn, ) - vjp_eps_complex = np.sum(d_eps_map.values) + vjp_eps_complex = d_eps_map.sum(dim=("x", "y", "z")) + + vjp_eps = 0.0 + vjp_sigma = 0.0 - freq = d_eps_map.coords["f"][0] - vjp_eps, vjp_sigma = self.eps_complex_to_eps_sigma(vjp_eps_complex, freq) + for freq in d_eps_map.coords["f"]: + vjp_eps_complex_f = vjp_eps_complex.sel(f=freq) + _vjp_eps, _vjp_sigma = self.eps_complex_to_eps_sigma(vjp_eps_complex_f, freq) + vjp_eps += _vjp_eps + vjp_sigma += _vjp_sigma return self.copy( update=dict( @@ -274,9 +288,19 @@ def store_vjp( inside_fn=inside_fn, ) - vjp_eps_complex_ii = np.sum(e_mult_dim.values) + vjp_eps_complex_ii = e_mult_dim.sum(dim=("x", "y", "z")) freq = e_mult_dim.coords["f"][0] - vjp_eps_ii, vjp_sigma_ii = self.eps_complex_to_eps_sigma(vjp_eps_complex_ii, freq) + + vjp_eps_ii = 0.0 + vjp_sigma_ii = 0.0 + + for freq in e_mult_dim.coords["f"]: + vjp_eps_complex_ii_f = vjp_eps_complex_ii.sel(f=freq) + _vjp_eps_ii, _vjp_sigma_ii = self.eps_complex_to_eps_sigma( + vjp_eps_complex_ii_f, freq + ) + vjp_eps_ii += _vjp_eps_ii + vjp_sigma_ii += _vjp_sigma_ii vjp_fields[component_name] = JaxMedium( permittivity=vjp_eps_ii, @@ -511,14 +535,18 @@ def store_vjp( # grab the correpsonding dotted fields at these interp_coords and sum over len-1 pixels field_name = "E" + dim - e_dotted = self.e_mult_volume( - field=field_name, - grad_data_fwd=grad_data_fwd, - grad_data_adj=grad_data_adj, - vol_coords=interp_coords, - d_vol=d_vols, - inside_fn=inside_fn, - ).sum(sum_axes) + e_dotted = ( + self.e_mult_volume( + field=field_name, + grad_data_fwd=grad_data_fwd, + grad_data_adj=grad_data_adj, + vol_coords=interp_coords, + d_vol=d_vols, + inside_fn=inside_fn, + ) + .sum(sum_axes) + .sum(dim="f") + ) # reshape values to the expected vjp shape to be more safe vjp_shape = tuple(len(coord) for _, coord in coords.items()) diff --git a/tidy3d/plugins/adjoint/components/simulation.py b/tidy3d/plugins/adjoint/components/simulation.py index 030d70939..2b8f422cf 100644 --- a/tidy3d/plugins/adjoint/components/simulation.py +++ b/tidy3d/plugins/adjoint/components/simulation.py @@ -1,7 +1,7 @@ """Defines a jax-compatible simulation.""" from __future__ import annotations -from typing import Tuple, Union, List, Dict +from typing import Tuple, Union, List, Dict, Literal from multiprocessing import Pool import pydantic.v1 as pd @@ -17,23 +17,42 @@ from ....components.data.monitor_data import FieldData, PermittivityData from ....components.structure import Structure from ....components.types import Ax, annotate_type -from ....constants import HERTZ +from ....constants import HERTZ, SECOND from ....exceptions import AdjointError from .base import JaxObject -from .structure import JaxStructure +from .structure import ( + JaxStructure, + JaxStructureType, + JaxStructureStaticMedium, + JaxStructureStaticGeometry, +) from .geometry import JaxPolySlab, JaxGeometryGroup -# bandwidth of adjoint source in units of freq0 if no sources and no `fwidth_adjoint` specified +# bandwidth of adjoint source in units of freq0 if no `fwidth_adjoint`, and one output freq FWIDTH_FACTOR = 1.0 / 10 +# bandwidth of adjoint sources in units of the minimum difference between output frequencies +FWIDTH_FACTOR_MULTIFREQ = 0.1 + +# the adjoint run time is RUN_TIME_FACTOR / fwidth +RUN_TIME_FACTOR = 100 + # how many processors to use for server and client side adjoint NUM_PROC_LOCAL = 1 # number of input structures before it errors MAX_NUM_INPUT_STRUCTURES = 400 +# generic warning for nonlinearity +NL_WARNING = ( + "The 'adjoint' plugin does not currently support nonlinear materials. " + "While the gradients might be calculated, they will be inaccurate and the " + "error will increase as the strength of the nonlinearity is increased. " + "We strongly recommend using linear simulations only with the adjoint plugin." +) + class JaxInfo(Tidy3dBaseModel): """Class to store information when converting between jax and tidy3d.""" @@ -69,12 +88,27 @@ class JaxInfo(Tidy3dBaseModel): units=HERTZ, ) + run_time_adjoint: float = pd.Field( + None, + title="Adjoint Run Time", + description="Custom run time of the original JaxSimulation.", + units=SECOND, + ) + + input_structure_types: Tuple[ + Literal["JaxStructure", "JaxStructureStaticMedium", "JaxStructureStaticGeometry"], ... + ] = pd.Field( + (), + title="Input Structure Types", + description="Type of the original input_structures (as strings).", + ) + @register_pytree_node_class class JaxSimulation(Simulation, JaxObject): """A :class:`.Simulation` registered with jax.""" - input_structures: Tuple[JaxStructure, ...] = pd.Field( + input_structures: Tuple[annotate_type(JaxStructureType), ...] = pd.Field( (), title="Input Structures", description="Tuple of jax-compatible structures" @@ -105,33 +139,18 @@ class JaxSimulation(Simulation, JaxObject): fwidth_adjoint: pd.PositiveFloat = pd.Field( None, title="Adjoint Frequency Width", - description="Custom frequency width to use for 'source_time' of adjoint sources. " - "If not supplied or 'None', uses the average fwidth of the original simulation's sources.", + description="Custom frequency width to use for ``source_time`` of adjoint sources. " + "If not supplied or ``None``, uses the average fwidth of the original simulation's sources.", units=HERTZ, ) - @pd.validator("output_monitors", always=True) - def _output_monitors_single_freq(cls, val): - """Assert all output monitors have just one frequency.""" - for mnt in val: - if len(mnt.freqs) != 1: - raise AdjointError( - "All output monitors must have single frequency for adjoint feature. " - f"Monitor '{mnt.name}' had {len(mnt.freqs)} frequencies." - ) - return val - - @pd.validator("output_monitors", always=True) - def _output_monitors_same_freq(cls, val): - """Assert all output monitors have the same frequency.""" - freqs = [mnt.freqs[0] for mnt in val] - if len(set(freqs)) > 1: - raise AdjointError( - "All output monitors must have the same frequency, " - f"given frequencies of {[f'{f:.2e}' for f in freqs]} (Hz) " - f"for monitors named '{[mnt.name for mnt in val]}', respectively." - ) - return val + run_time_adjoint: pd.PositiveFloat = pd.Field( + None, + title="Adjoint Run Time", + description="Custom ``run_time`` to use for adjoint simulation. " + "If not supplied or ``None``, uses a factor times the adjoint source ``fwidth``.", + units=SECOND, + ) @pd.validator("output_monitors", always=True) def _output_monitors_colocate_false(cls, val): @@ -173,7 +192,8 @@ def _restrict_input_structures(cls, val): def _warn_overlap(cls, val, values): """Print appropriate warning if structures intersect in ways that cause gradient error.""" - input_structures = list(val) + input_structures = [s for s in val if "geometry" in s._differentiable_fields] + structures = list(values.get("structures")) # if the center and size of all structure geometries do not contain all numbers, skip check @@ -226,19 +246,72 @@ def _warn_if_colocate(cls, val): return val return val + @pd.validator("medium", always=True) + def _warn_nonlinear_medium(cls, val): + """warn if the jax simulation medium is nonlinear.""" + # hasattr is just an additional check to avoid unnecessary bugs + # if a medium is encountered that doesnt support nonlinear spec, or things change. + if hasattr(val, "nonlinear_spec") and val.nonlinear_spec: + log.warning( + "Nonlinear background medium detected in the 'JaxSimulation'. " + NL_WARNING + ) + return val + + @pd.validator("structures", always=True) + def _warn_nonlinear_structure(cls, val): + """warn if a jax simulation structure.medium is nonlinear.""" + for i, struct in enumerate(val): + medium = struct.medium + # hasattr is just an additional check to avoid unnecessary bugs + # if a medium is encountered that doesnt support nonlinear spec, or things change. + if hasattr(medium, "nonlinear_spec") and medium.nonlinear_spec: + log.warning(f"Nonlinear medium detected in structures[{i}]. " + NL_WARNING) + return val + + @pd.validator("input_structures", always=True) + def _warn_nonlinear_input_structure(cls, val): + """warn if a jax simulation input_structure.medium is nonlinear.""" + for i, struct in enumerate(val): + medium = struct.medium + # hasattr is just an additional check to avoid unnecessary bugs + # if a medium is encountered that doesnt support nonlinear spec, or things change. + if hasattr(medium, "nonlinear_spec") and medium.nonlinear_spec: + log.warning(f"Nonlinear medium detected in input_structures[{i}]. " + NL_WARNING) + return val + @staticmethod - def get_freq_adjoint(output_monitors: List[Monitor]) -> float: - """Return the single adjoint frequency stripped from the output monitors.""" + def get_freqs_adjoint(output_monitors: List[Monitor]) -> List[float]: + """Return sorted list of unique frequencies stripped from a collection of monitors.""" if len(output_monitors) == 0: raise AdjointError("Can't get adjoint frequency as no output monitors present.") - return output_monitors[0].freqs[0] + output_freqs = [] + for mnt in output_monitors: + for freq in mnt.freqs: + output_freqs.append(freq) + + return np.unique(output_freqs).tolist() + + @cached_property + def freqs_adjoint(self) -> List[float]: + """Return sorted list of frequencies stripped from the output monitors.""" + return self.get_freqs_adjoint(output_monitors=self.output_monitors) @cached_property - def freq_adjoint(self) -> float: - """Return the single adjoint frequency stripped from the output monitors.""" - return self.get_freq_adjoint(output_monitors=self.output_monitors) + def _is_multi_freq(self) -> bool: + """Does this simulation have a multi-frequency output?""" + return len(self.freqs_adjoint) > 1 + + @cached_property + def _min_delta_freq(self) -> float: + """Minimum spacing between output_frequencies (Hz).""" + + if not self._is_multi_freq: + return None + + delta_freqs = np.abs(np.diff(np.sort(np.array(self.freqs_adjoint)))) + return np.min(delta_freqs) @cached_property def _fwidth_adjoint(self) -> float: @@ -248,19 +321,51 @@ def _fwidth_adjoint(self) -> float: if self.fwidth_adjoint is not None: return self.fwidth_adjoint - # otherwise, grab from sources - num_sources = len(self.sources) + freqs_adjoint = self.freqs_adjoint + + # multiple output frequency case + if self._is_multi_freq: + return FWIDTH_FACTOR_MULTIFREQ * self._min_delta_freq + + # otherwise, grab from sources and output monitors + num_sources = len(self.sources) # should be 0 for adjoint already but worth checking - # if no sources, just use a constant factor times the adjoint frequency + # if no sources, just use a constant factor times the mean adjoint frequency if num_sources == 0: - return FWIDTH_FACTOR * self.freq_adjoint + return FWIDTH_FACTOR * np.mean(freqs_adjoint) - # if more than one forward source, use their average + # if more than one forward source, use their maximum if num_sources > 1: - log.warning(f"{num_sources} sources, using their average 'fwidth' for adjoint source.") + log.warning(f"{num_sources} sources, using their maximum 'fwidth' for adjoint source.") fwidths = [src.source_time.fwidth for src in self.sources] - return np.mean(fwidths) + return np.max(fwidths) + + @cached_property + def _run_time_adjoint(self: float) -> float: + """Return the run time of the adjoint simulation as a function of its fwidth.""" + + if self.run_time_adjoint is not None: + return self.run_time_adjoint + + run_time_adjoint = RUN_TIME_FACTOR / self._fwidth_adjoint + + if self._is_multi_freq: + + log.warning( + f"{len(self.freqs_adjoint)} unique frequencies detected in the output monitors " + f"with a minimum spacing of {self._min_delta_freq:.3e} (Hz). " + f"Setting the 'fwidth' of the adjoint sources to {FWIDTH_FACTOR_MULTIFREQ} times " + f"this value = {self._fwidth_adjoint:.3e} (Hz) to avoid spectral overlap. " + "To account for this, the corresponding 'run_time' in the adjoint simulation is " + f"will be set to {run_time_adjoint:3e} " + f"compared to {self.run_time:3e} in the forward simulation. " + "If the adjoint 'run_time' is large due to small frequency spacing, " + "it could be better to instead run one simulation per frequency, " + "which can be done in parallel using 'tidy3d.plugins.adjoint.web.run_async'." + ) + + return run_time_adjoint def to_simulation(self) -> Tuple[Simulation, JaxInfo]: """Convert :class:`.JaxSimulation` instance to :class:`.Simulation` with an info dict.""" @@ -275,8 +380,9 @@ def to_simulation(self) -> Tuple[Simulation, JaxInfo]: "grad_eps_monitors", "input_structures", "fwidth_adjoint", + "run_time_adjoint", } - ) # .copy() + ) sim = Simulation.parse_obj(sim_dict) # put all structures and monitors in one list @@ -288,7 +394,7 @@ def to_simulation(self) -> Tuple[Simulation, JaxInfo]: + list(self.grad_eps_monitors) ) - sim = sim.copy(update=dict(structures=all_structures, monitors=all_monitors)) + sim = sim.updated_copy(structures=all_structures, monitors=all_monitors) # information about the state of the original JaxSimulation to stash for reconstruction jax_info = JaxInfo( @@ -297,6 +403,8 @@ def to_simulation(self) -> Tuple[Simulation, JaxInfo]: num_grad_monitors=len(self.grad_monitors), num_grad_eps_monitors=len(self.grad_eps_monitors), fwidth_adjoint=self.fwidth_adjoint, + run_time_adjoint=self.run_time_adjoint, + input_structure_types=[s.type for s in self.input_structures], ) return sim, jax_info @@ -504,7 +612,19 @@ def split_structures( # split the list based on these numbers structures = all_structures[:num_structs] - input_structures = [JaxStructure.from_structure(s) for s in all_structures[num_structs:]] + structure_type_map = dict( + JaxStructure=JaxStructure, + JaxStructureStaticMedium=JaxStructureStaticMedium, + JaxStructureStaticGeometry=JaxStructureStaticGeometry, + ) + + input_structures = [] + for struct_type_str, struct in zip( + jax_info.input_structure_types, all_structures[num_structs:] + ): + struct_type = structure_type_map[struct_type_str] + new_structure = struct_type.from_structure(struct) + input_structures.append(new_structure) # return a dictionary containing these split structures return dict(structures=structures, input_structures=input_structures) @@ -522,7 +642,12 @@ def from_simulation(cls, simulation: Simulation, jax_info: JaxInfo) -> JaxSimula # update the dictionary with these and the adjoint fwidth sim_dict.update(**structures) sim_dict.update(**monitors) - sim_dict.update(dict(fwidth_adjoint=jax_info.fwidth_adjoint)) + sim_dict.update( + dict( + fwidth_adjoint=jax_info.fwidth_adjoint, + run_time_adjoint=jax_info.run_time_adjoint, + ) + ) # load JaxSimulation from the dictionary return cls.parse_obj(sim_dict) @@ -539,7 +664,7 @@ def make_sim_fwd(cls, simulation: Simulation, jax_info: JaxInfo) -> Tuple[Simula input_structures = structure_dict["input_structures"] grad_mnt_dict = cls.get_grad_monitors( input_structures=input_structures, - freq_adjoint=cls.get_freq_adjoint(output_monitors=output_monitors), + freqs_adjoint=cls.get_freqs_adjoint(output_monitors=output_monitors), ) grad_mnts = grad_mnt_dict["grad_monitors"] @@ -569,14 +694,14 @@ def to_simulation_fwd(self) -> Tuple[Simulation, JaxInfo, JaxInfo]: @staticmethod def get_grad_monitors( - input_structures: List[Structure], freq_adjoint: float, include_eps_mnts: bool = True + input_structures: List[Structure], freqs_adjoint: List[float], include_eps_mnts: bool = True ) -> dict: """Return dictionary of gradient monitors for simulation.""" grad_mnts = [] grad_eps_mnts = [] for index, structure in enumerate(input_structures): grad_mnt, grad_eps_mnt = structure.make_grad_monitors( - freq=freq_adjoint, name=f"grad_mnt_{index}" + freqs=freqs_adjoint, name=f"grad_mnt_{index}" ) grad_mnts.append(grad_mnt) if include_eps_mnts: @@ -593,8 +718,8 @@ def _store_vjp_structure( ) -> JaxStructure: """Store the vjp for a single structure.""" - freq = float(eps_data.eps_xx.coords["f"]) - eps_out = self.medium.eps_model(frequency=freq) + freq_max = float(max(eps_data.eps_xx.coords["f"])) + eps_out = self.medium.eps_model(frequency=freq_max) return structure.store_vjp( grad_data_fwd=fld_fwd, grad_data_adj=fld_adj, diff --git a/tidy3d/plugins/adjoint/components/structure.py b/tidy3d/plugins/adjoint/components/structure.py index 1438a6896..c358de6cc 100644 --- a/tidy3d/plugins/adjoint/components/structure.py +++ b/tidy3d/plugins/adjoint/components/structure.py @@ -1,6 +1,8 @@ """Defines a jax-compatible structure and its conversion to a gradient monitor.""" from __future__ import annotations +from typing import List, Union, Dict + import pydantic.v1 as pd import numpy as np from jax.tree_util import register_pytree_node_class @@ -10,60 +12,91 @@ from ....components.monitor import FieldMonitor from ....components.data.monitor_data import FieldData, PermittivityData from ....components.types import Bound, TYPE_TAG_STR +from ....components.medium import MediumType +from ....components.geometry.utils import GeometryType from .base import JaxObject from .medium import JaxMediumType, JAX_MEDIUM_MAP -from .geometry import JaxGeometryType, JAX_GEOMETRY_MAP +from .geometry import JaxGeometryType, JAX_GEOMETRY_MAP, JaxBox +GEO_MED_MAPPINGS = dict(geometry=JAX_GEOMETRY_MAP, medium=JAX_MEDIUM_MAP) -@register_pytree_node_class -class JaxStructure(Structure, JaxObject): + +class AbstractJaxStructure(Structure, JaxObject): """A :class:`.Structure` registered with jax.""" - geometry: JaxGeometryType = pd.Field( - ..., - title="Geometry", - description="Geometry of the structure, which is jax-compatible.", - jax_field=True, - discriminator=TYPE_TAG_STR, - ) + geometry: Union[JaxGeometryType, GeometryType] + medium: Union[JaxMediumType, MediumType] - medium: JaxMediumType = pd.Field( - ..., - title="Medium", - description="Medium of the structure, which is jax-compatible.", - jax_field=True, - discriminator=TYPE_TAG_STR, - ) + # which of "geometry" or "medium" is differentiable for this class + _differentiable_fields = () + + @pd.validator("medium", always=True) + def _check_2d_geometry(cls, val, values): + """Override validator checking 2D geometry, which triggers unnecessarily for gradients.""" + return val + + @property + def jax_fields(self): + """The fields that are jax-traced for this class.""" + return dict(geometry=self.geometry, medium=self.medium) + + @property + def exclude_fields(self): + """Fields to exclude from the self dict.""" + return set(["type"] + list(self.jax_fields.keys())) def to_structure(self) -> Structure: """Convert :class:`.JaxStructure` instance to :class:`.Structure`""" - self_dict = self.dict(exclude={"type", "geometry", "medium"}) - self_dict["geometry"] = self.geometry.to_tidy3d() - self_dict["medium"] = self.medium.to_medium() + self_dict = self.dict(exclude=self.exclude_fields) + for key, component in self.jax_fields.items(): + if key in self._differentiable_fields: + self_dict[key] = component.to_tidy3d() + else: + self_dict[key] = component return Structure.parse_obj(self_dict) @classmethod def from_structure(cls, structure: Structure) -> JaxStructure: """Convert :class:`.Structure` to :class:`.JaxStructure`.""" - # get the appropriate jax types corresponding to the td.Structure fields - jax_geometry_type = JAX_GEOMETRY_MAP[type(structure.geometry)] - jax_medium_type = JAX_MEDIUM_MAP[type(structure.medium)] + struct_dict = structure.dict(exclude={"type"}) + + jax_fields = dict(geometry=structure.geometry, medium=structure.medium) - # load them into the JaxStructure dictionary and parse it into an instance - struct_dict = structure.dict(exclude={"type", "geometry", "medium"}) - struct_dict["geometry"] = jax_geometry_type.from_tidy3d(structure.geometry) - struct_dict["medium"] = jax_medium_type.from_tidy3d(structure.medium) + for key, component in jax_fields.items(): + if key in cls._differentiable_fields: + type_map = GEO_MED_MAPPINGS[key] + jax_type = type_map[type(component)] + struct_dict[key] = jax_type.from_tidy3d(component) + else: + struct_dict[key] = component return cls.parse_obj(struct_dict) - @pd.validator("medium", always=True) - def _check_2d_geometry(cls, val, values): - """Override validator checking 2D geometry, which triggers unnecessarily for gradients.""" - return val + def make_grad_monitors(self, freqs: List[float], name: str) -> FieldMonitor: + """Return gradient monitor associated with this object.""" + if "geometry" not in self._differentiable_fields: + # make a fake JaxBox to be able to call .make_grad_monitors + rmin, rmax = self.geometry.bounds + geometry = JaxBox.from_bounds(rmin=rmin, rmax=rmax) + else: + geometry = self.geometry + return geometry.make_grad_monitors(freqs=freqs, name=name) + + def _get_medium_params( + self, + grad_data_eps: PermittivityData, + ) -> Dict[str, float]: + """Compute params in the material of this structure.""" + freq_max = max(grad_data_eps.eps_xx.f) + eps_in = self.medium.eps_model(frequency=freq_max) + ref_ind = np.sqrt(np.max(np.real(eps_in))) + wvl_free_space = C_0 / freq_max + wvl_mat = wvl_free_space / ref_ind + return dict(wvl_mat=wvl_mat, eps_in=eps_in) - def store_vjp( + def geometry_vjp( self, grad_data_fwd: FieldData, grad_data_adj: FieldData, @@ -71,37 +104,150 @@ def store_vjp( sim_bounds: Bound, eps_out: complex, num_proc: int = 1, - ) -> JaxStructure: - """Returns the gradient of the structure parameters given forward and adjoint field data.""" + ) -> JaxGeometryType: + """Compute the VJP for the structure geometry.""" - # compute wavelength in material (to use for determining integration points) - freq = float(grad_data_eps.eps_xx.f) - wvl_free_space = C_0 / freq - eps_in = self.medium.eps_model(frequency=freq) - ref_ind = np.sqrt(np.max(np.real(eps_in))) - wvl_mat = wvl_free_space / ref_ind + medium_params = self._get_medium_params(grad_data_eps=grad_data_eps) - geo_vjp = self.geometry.store_vjp( + return self.geometry.store_vjp( grad_data_fwd=grad_data_fwd, grad_data_adj=grad_data_adj, grad_data_eps=grad_data_eps, sim_bounds=sim_bounds, - wvl_mat=wvl_mat, + wvl_mat=medium_params["wvl_mat"], eps_out=eps_out, - eps_in=eps_in, + eps_in=medium_params["eps_in"], num_proc=num_proc, ) - medium_vjp = self.medium.store_vjp( + def medium_vjp( + self, + grad_data_fwd: FieldData, + grad_data_adj: FieldData, + grad_data_eps: PermittivityData, + sim_bounds: Bound, + ) -> JaxMediumType: + """Compute the VJP for the structure medium.""" + + medium_params = self._get_medium_params(grad_data_eps=grad_data_eps) + + return self.medium.store_vjp( grad_data_fwd=grad_data_fwd, grad_data_adj=grad_data_adj, sim_bounds=sim_bounds, - wvl_mat=wvl_mat, + wvl_mat=medium_params["wvl_mat"], inside_fn=self.geometry.inside, ) - return self.copy(update=dict(geometry=geo_vjp, medium=medium_vjp)) + def store_vjp( + self, + # field_keys: List[Literal["medium", "geometry"]], + grad_data_fwd: FieldData, + grad_data_adj: FieldData, + grad_data_eps: PermittivityData, + sim_bounds: Bound, + eps_out: complex, + num_proc: int = 1, + ) -> JaxStructure: + """Returns the gradient of the structure parameters given forward and adjoint field data.""" - def make_grad_monitors(self, freq: float, name: str) -> FieldMonitor: - """Return gradient monitor associated with this object.""" - return self.geometry.make_grad_monitors(freq=freq, name=name) + # return right away if field_keys are not present for some reason + if not self._differentiable_fields: + return self + + vjp_dict = {} + + # compute minimum wavelength in material (to use for determining integration points) + if "geometry" in self._differentiable_fields: + vjp_dict["geometry"] = self.geometry_vjp( + grad_data_fwd=grad_data_fwd, + grad_data_adj=grad_data_adj, + grad_data_eps=grad_data_eps, + sim_bounds=sim_bounds, + eps_out=eps_out, + num_proc=num_proc, + ) + + if "medium" in self._differentiable_fields: + vjp_dict["medium"] = self.medium_vjp( + grad_data_fwd=grad_data_fwd, + grad_data_adj=grad_data_adj, + grad_data_eps=grad_data_eps, + sim_bounds=sim_bounds, + ) + + return self.updated_copy(**vjp_dict) + + +@register_pytree_node_class +class JaxStructure(AbstractJaxStructure, JaxObject): + """A :class:`.Structure` registered with jax.""" + + geometry: JaxGeometryType = pd.Field( + ..., + title="Geometry", + description="Geometry of the structure, which is jax-compatible.", + jax_field=True, + discriminator=TYPE_TAG_STR, + ) + + medium: JaxMediumType = pd.Field( + ..., + title="Medium", + description="Medium of the structure, which is jax-compatible.", + jax_field=True, + discriminator=TYPE_TAG_STR, + ) + + _differentiable_fields = ("medium", "geometry") + + +@register_pytree_node_class +class JaxStructureStaticMedium(AbstractJaxStructure, JaxObject): + """A :class:`.Structure` registered with jax.""" + + geometry: JaxGeometryType = pd.Field( + ..., + title="Geometry", + description="Geometry of the structure, which is jax-compatible.", + jax_field=True, + discriminator=TYPE_TAG_STR, + ) + + medium: MediumType = pd.Field( + ..., + title="Medium", + description="Regular ``tidy3d`` medium of the structure, non differentiable. " + "Supports dispersive materials.", + jax_field=False, + discriminator=TYPE_TAG_STR, + ) + + _differentiable_fields = ("geometry",) + + +@register_pytree_node_class +class JaxStructureStaticGeometry(AbstractJaxStructure, JaxObject): + """A :class:`.Structure` registered with jax.""" + + geometry: GeometryType = pd.Field( + ..., + title="Geometry", + description="Regular ``tidy3d`` geometry of the structure, non differentiable. " + "Supports angled sidewalls and other complex geometries.", + jax_field=False, + discriminator=TYPE_TAG_STR, + ) + + medium: JaxMediumType = pd.Field( + ..., + title="Medium", + description="Medium of the structure, which is jax-compatible.", + jax_field=True, + discriminator=TYPE_TAG_STR, + ) + + _differentiable_fields = ("medium",) + + +JaxStructureType = Union[JaxStructure, JaxStructureStaticMedium, JaxStructureStaticGeometry] diff --git a/tidy3d/plugins/adjoint/utils/filter.py b/tidy3d/plugins/adjoint/utils/filter.py index 89487d07e..dc81f66a2 100644 --- a/tidy3d/plugins/adjoint/utils/filter.py +++ b/tidy3d/plugins/adjoint/utils/filter.py @@ -8,6 +8,7 @@ from ....components.base import Tidy3dBaseModel from ....constants import MICROMETER +from ....log import log class Filter(Tidy3dBaseModel, ABC): @@ -59,6 +60,39 @@ def _deprecate_feature_size(cls, values): def make_kernel(self, coords_rad: jnp.array) -> jnp.array: """Function to make the kernel out of a coordinate grid of radius values.""" + @staticmethod + def _check_kernel_size(kernel: jnp.array, signal_in: jnp.array) -> jnp.array: + """Make sure kernel isn't larger than signal and warn and truncate if so.""" + + kernel_shape = kernel.shape + input_shape = signal_in.shape + + if any((k_shape > in_shape for k_shape, in_shape in zip(kernel_shape, input_shape))): + + # remove some pixels from the kernel to make things right + new_kernel = kernel.copy() + for axis, (len_kernel, len_input) in enumerate(zip(kernel_shape, input_shape)): + if len_kernel > len_input: + rm_pixels_total = len_kernel - len_input + rm_pixels_edge = int(np.ceil(rm_pixels_total / 2)) + indices_truncated = np.arange(rm_pixels_edge, len_kernel - rm_pixels_edge) + new_kernel = new_kernel.take(indices=indices_truncated.astype(int), axis=axis) + + log.warning( + f"The filter input has shape {input_shape} whereas the " + f"kernel has shape {kernel_shape}. " + "These shapes are incompatible as the input must " + "be larger than the kernel along all dimensions. " + "The kernel will automatically be " + f"resized to {new_kernel.shape} to be less than the input shape. " + "If this is unexpected, " + "either reduce the filter 'radius' or increase the input array's size." + ) + + return new_kernel + + return kernel + def evaluate(self, spatial_data: jnp.array) -> jnp.array: """Process on supplied spatial data.""" @@ -74,6 +108,9 @@ def evaluate(self, spatial_data: jnp.array) -> jnp.array: # construct the kernel kernel = self.make_kernel(coords_rad) + # handle when kernel is too large compared to input + kernel = self._check_kernel_size(kernel=kernel, signal_in=rho) + # normalize by the kernel operating on a spatial_data of all ones num = jsp.signal.convolve(rho, kernel, mode="same") den = jsp.signal.convolve(jnp.ones_like(rho), kernel, mode="same") diff --git a/tidy3d/plugins/adjoint/web.py b/tidy3d/plugins/adjoint/web.py index 16abcf0b5..87dd5401b 100644 --- a/tidy3d/plugins/adjoint/web.py +++ b/tidy3d/plugins/adjoint/web.py @@ -9,11 +9,11 @@ from ...components.simulation import Simulation from ...components.data.sim_data import SimulationData -from ...web.webapi import run as web_run -from ...web.webapi import wait_for_connection -from ...web.s3utils import download_file, upload_file -from ...web.asynchronous import run_async as web_run_async -from ...web.container import BatchData, DEFAULT_DATA_DIR, Job, Batch +from tidy3d.web.api.webapi import run as web_run +from tidy3d.web.api.webapi import wait_for_connection +from tidy3d.web.core.s3utils import download_file, upload_file +from tidy3d.web.api.asynchronous import run_async as web_run_async +from ...web.api.container import BatchData, DEFAULT_DATA_DIR, Job, Batch from ...components.types import Literal from .components.base import JaxObject @@ -165,7 +165,8 @@ def run_bwd( fwd_task_id = res[0].fwd_task_id fwidth_adj = sim_data_vjp.simulation._fwidth_adjoint - jax_sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj) + run_time_adj = sim_data_vjp.simulation._run_time_adjoint + jax_sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj, run_time=run_time_adj) sim_adj, jax_info_adj = jax_sim_adj.to_simulation() sim_vjp = webapi_run_adjoint_bwd( @@ -484,7 +485,8 @@ def run_async_bwd( for sim_data_vjp, fwd_task_id in zip(batch_data_vjp, fwd_task_ids): parent_tasks_adj.append([str(fwd_task_id)]) fwidth_adj = sim_data_vjp.simulation._fwidth_adjoint - jax_sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj) + run_time_adj = sim_data_vjp.simulation._run_time_adjoint + jax_sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj, run_time=run_time_adj) sim_adj, jax_info_adj = jax_sim_adj.to_simulation() sims_adj.append(sim_adj) jax_infos_adj.append(jax_info_adj) @@ -647,7 +649,7 @@ def run_local_fwd( # add the gradient monitors and run the forward simulation grad_mnts = simulation.get_grad_monitors( - input_structures=simulation.input_structures, freq_adjoint=simulation.freq_adjoint + input_structures=simulation.input_structures, freqs_adjoint=simulation.freqs_adjoint ) sim_fwd = simulation.updated_copy(**grad_mnts) sim_data_fwd = run( @@ -682,7 +684,8 @@ def run_local_bwd( # make and run adjoint simulation fwidth_adj = sim_data_fwd.simulation._fwidth_adjoint - sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj) + run_time_adj = sim_data_fwd.simulation._run_time_adjoint + sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj, run_time=run_time_adj) sim_data_adj = run( simulation=sim_adj, task_name=_task_name_adj(task_name), @@ -691,6 +694,9 @@ def run_local_bwd( callback_url=callback_url, verbose=verbose, ) + + sim_data_adj = sim_data_adj.normalize_adjoint_fields() + grad_data_adj = sim_data_adj.grad_data_symmetry # get gradient and insert into the resulting simulation structure medium @@ -804,7 +810,7 @@ def run_async_local_fwd( for simulation in simulations: grad_mnts = simulation.get_grad_monitors( - input_structures=simulation.input_structures, freq_adjoint=simulation.freq_adjoint + input_structures=simulation.input_structures, freqs_adjoint=simulation.freqs_adjoint ) sim_fwd = simulation.updated_copy(**grad_mnts) sims_fwd.append(sim_fwd) @@ -857,8 +863,9 @@ def run_async_local_bwd( sims_adj = [] for i, sim_data_fwd in enumerate(batch_data_fwd): fwidth_adj = sim_data_fwd.simulation._fwidth_adjoint + run_time_adj = sim_data_fwd.simulation._run_time_adjoint sim_data_vjp = batch_data_vjp[i] - sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj) + sim_adj = sim_data_vjp.make_adjoint_simulation(fwidth=fwidth_adj, run_time=run_time_adj) sims_adj.append(sim_adj) batch_data_adj = run_async_local( @@ -874,6 +881,8 @@ def run_async_local_bwd( sims_vjp = [] for i, (sim_data_fwd, sim_data_adj) in enumerate(zip(batch_data_fwd, batch_data_adj)): + sim_data_adj = sim_data_adj.normalize_adjoint_fields() + grad_data_fwd = sim_data_fwd.grad_data_symmetry grad_data_adj = sim_data_adj.grad_data_symmetry grad_data_eps_fwd = sim_data_fwd.grad_eps_data_symmetry diff --git a/tidy3d/plugins/dispersion/fit.py b/tidy3d/plugins/dispersion/fit.py index a0123b93e..d648ff862 100644 --- a/tidy3d/plugins/dispersion/fit.py +++ b/tidy3d/plugins/dispersion/fit.py @@ -18,7 +18,7 @@ from ...components.types import Ax, ArrayFloat1D from ...constants import C_0, HBAR, MICROMETER from ...exceptions import ValidationError, WebError, SetupError -from ...web.environment import Env +from tidy3d.web.core.environment import Env class DispersionFitter(Tidy3dBaseModel): diff --git a/tidy3d/plugins/dispersion/fit_fast.py b/tidy3d/plugins/dispersion/fit_fast.py index 71e99bda2..2bbe49a7c 100644 --- a/tidy3d/plugins/dispersion/fit_fast.py +++ b/tidy3d/plugins/dispersion/fit_fast.py @@ -149,10 +149,12 @@ class FastFitterData(AdvancedFastFitterParam): title="eps_inf", description="Value of ``eps_inf``.", ) - poles: ArrayComplex1D = Field( + poles: Optional[ArrayComplex1D] = Field( None, title="Pole frequencies in eV", description="Pole frequencies in eV" ) - residues: ArrayComplex1D = Field(None, title="Residues in eV", description="Residues in eV") + residues: Optional[ArrayComplex1D] = Field( + None, title="Residues in eV", description="Residues in eV" + ) passivity_optimized: Optional[bool] = Field( False, @@ -188,7 +190,11 @@ def _generate_initial_poles(cls, val, values): """Generate initial poles.""" if val is not None: return val - if values["logspacing"] is None or values["smooth"] is None: + if ( + values.get("logspacing") is None + or values.get("smooth") is None + or values.get("num_poles") is None + ): return None omega = values["omega"] num_poles = values["num_poles"] @@ -211,7 +217,7 @@ def _generate_initial_residues(cls, val, values): """Generate initial residues.""" if val is not None: return val - poles = values["poles"] + poles = values.get("poles") if poles is None: return None return np.zeros(len(poles)) @@ -675,6 +681,11 @@ def fit( Best fitting result: (dispersive medium, weighted RMS error). """ + if max_num_poles < min_num_poles: + raise ValidationError( + "Dispersion fitter cannot have 'max_num_poles' less than 'min_num_poles'." + ) + omega = PoleResidue.angular_freq_to_eV(PoleResidue.Hz_to_angular_freq(self.freqs[::-1])) eps = self.eps_data[::-1] diff --git a/tidy3d/plugins/dispersion/web.py b/tidy3d/plugins/dispersion/web.py index be9558c7c..fbbf9c58e 100644 --- a/tidy3d/plugins/dispersion/web.py +++ b/tidy3d/plugins/dispersion/web.py @@ -15,8 +15,8 @@ from ...components.medium import PoleResidue from ...constants import MICROMETER, HERTZ from ...exceptions import WebError, Tidy3dError, SetupError -from ...web.http_management import get_headers -from ...web.environment import Env +from tidy3d.web.core.http_util import get_headers +from tidy3d.web.core.environment import Env from .fit import DispersionFitter diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index a3df959f3..b4395d874 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import List, Tuple, Dict +from math import isclose import numpy as np import pydantic.v1 as pydantic @@ -16,6 +17,7 @@ from ...components.grid.grid import Grid from ...components.mode import ModeSpec from ...components.monitor import ModeSolverMonitor, ModeMonitor +from ...components.medium import FullyAnisotropicMedium from ...components.source import ModeSource, SourceTime from ...components.types import Direction, FreqArray, Ax, Literal, Axis, Symmetry, PlotScale from ...components.types import ArrayComplex3D, ArrayComplex4D, ArrayFloat1D, EpsSpecType @@ -23,20 +25,32 @@ from ...components.data.data_array import FreqModeDataArray from ...components.data.sim_data import SimulationData from ...components.data.monitor_data import ModeSolverData -from ...exceptions import ValidationError + +from ...components.validators import validate_freqs_min, validate_freqs_not_empty +from ...exceptions import ValidationError, SetupError from ...constants import C_0 -from .solver import compute_modes +# Importing the local solver may not work if e.g. scipy is not installed +IMPORT_ERROR_MSG = """Could not import local solver, 'ModeSolver' objects can still be constructed +but will have to be run through the server. +""" +try: + from .solver import compute_modes + + LOCAL_SOLVER_IMPORTED = True +except ImportError: + log.warning(IMPORT_ERROR_MSG) + LOCAL_SOLVER_IMPORTED = False FIELD = Tuple[ArrayComplex3D, ArrayComplex3D, ArrayComplex3D] MODE_MONITOR_NAME = "<<>>" -# Lowest frequency supported (Hz) -MIN_FREQUENCY = 1e5 - # Warning for field intensity at edges over total field intensity larger than this value FIELD_DECAY_CUTOFF = 1e-2 +# Maximum allowed size of the field data produced by the mode solver +MAX_MODES_DATA_SIZE_GB = 20 + class ModeSolver(Tidy3dBaseModel): """Interface for solving electromagnetic eigenmodes in a 2D plane with translational @@ -82,21 +96,18 @@ def is_plane(cls, val): raise ValidationError(f"ModeSolver plane must be planar, given size={val}") return val - @pydantic.validator("freqs", always=True) - def freqs_not_empty(cls, val): - """Raise validation error if ``freqs`` is an empty Tuple.""" - if len(val) == 0: - raise ValidationError("ModeSolver 'freqs' must be a non-empty tuple.") - return val + _freqs_not_empty = validate_freqs_not_empty() + _freqs_lower_bound = validate_freqs_min() - @pydantic.validator("freqs", always=True) - def freqs_lower_bound(cls, val): - """Raise validation error if any of ``freqs`` is lower than ``MIN_FREQUENCY``.""" - if min(val) < MIN_FREQUENCY: - raise ValidationError( - f"ModeSolver 'freqs' must be no lower than {MIN_FREQUENCY:.0e} Hz. " - "Note that the unit of frequency is 'Hz'." - ) + @pydantic.validator("plane", always=True) + def plane_in_sim_bounds(cls, val, values): + """Check that the plane is at least partially inside the simulation bounds.""" + sim_center = values.get("simulation").center + sim_size = values.get("simulation").size + sim_box = Box(size=sim_size, center=sim_center) + + if not sim_box.intersects(val): + raise SetupError("'ModeSolver.plane' must intersect 'ModeSolver.simulation'.") return val @cached_property @@ -143,6 +154,14 @@ def _solver_grid(self) -> Grid: return self.simulation._subgrid(span_inds=span_inds) + @cached_property + def _num_cells_freqs_modes(self) -> Tuple[int, int, int]: + """Get the number of spatial points, number of freqs, and number of modes requested.""" + num_cells = np.prod(self._solver_grid.num_cells) + num_modes = self.mode_spec.num_modes + num_freqs = len(self.freqs) + return num_cells, num_freqs, num_modes + def solve(self) -> ModeSolverData: """:class:`.ModeSolverData` containing the field and effective index data. @@ -192,6 +211,12 @@ def _get_data_with_group_index(self) -> ModeSolverData: return mode_solver.data_raw._group_index_post_process(self.mode_spec.group_index_step) + @cached_property + def grid_snapped(self) -> Grid: + """The solver grid snapped to the plane normal and to simulation 0-sized dims if any.""" + grid_snapped = self._solver_grid.snap_to_box_zero_dim(self.plane) + return self.simulation._snap_zero_dim(grid_snapped) + @cached_property def data_raw(self) -> ModeSolverData: """:class:`.ModeSolverData` containing the field and effective index on unexpanded grid. @@ -205,6 +230,30 @@ def data_raw(self) -> ModeSolverData: if self.mode_spec.group_index_step > 0: return self._get_data_with_group_index() + # Compute data on the Yee grid + mode_solver_data = self._data_on_yee_grid() + + # Colocate to grid boundaries if requested + if self.colocate: + mode_solver_data = self._colocate_data(mode_solver_data=mode_solver_data) + + # normalize modes + self._normalize_modes(mode_solver_data=mode_solver_data) + + # filter polarization if requested + if self.mode_spec.filter_pol is not None: + self._filter_polarization(mode_solver_data=mode_solver_data) + + # sort modes if requested + if self.mode_spec.track_freq and len(self.freqs) > 1: + mode_solver_data = mode_solver_data.overlap_sort(self.mode_spec.track_freq) + + self._field_decay_warning(mode_solver_data.symmetry_expanded) + + return mode_solver_data + + def _data_on_yee_grid(self) -> ModeSolverData: + """Solve for all modes, and construct data with fields on the Yee grid.""" _, _solver_coords = self.plane.pop_axis( self._solver_grid.boundaries.to_list, axis=self.normal_axis ) @@ -224,15 +273,9 @@ def data_raw(self) -> ModeSolverData: ) data_dict = {"n_complex": index_data} - # Construct and add all the data for the fields - # Snap the solver grid to plane normal and simulation 0-sized dims if any - grid_snapped = self._solver_grid.snap_to_box_zero_dim(self.plane) - - grid_snapped = self.simulation._snap_zero_dim(grid_snapped) - # Construct the field data on Yee grid for field_name in ("Ex", "Ey", "Ez", "Hx", "Hy", "Hz"): - xyz_coords = grid_snapped[field_name].to_list + xyz_coords = self.grid_snapped[field_name].to_list scalar_field_data = ScalarModeFieldDataArray( np.stack([field_freq[field_name] for field_freq in fields], axis=-2), coords=dict( @@ -254,7 +297,7 @@ def data_raw(self) -> ModeSolverData: direction=self.direction, ) - # make mode solver data on the Yee grid for now + # make mode solver data on the Yee grid mode_solver_monitor = self.to_mode_solver_monitor(name=MODE_MONITOR_NAME, colocate=False) grid_expanded = self.simulation.discretize_monitor(mode_solver_monitor) mode_solver_data = ModeSolverData( @@ -268,64 +311,68 @@ def data_raw(self) -> ModeSolverData: **data_dict, ) - # Colocate to grid boundaries if requested - if self.colocate: - # Get colocation coordinates in the solver plane - _, plane_dims = self.plane.pop_axis("xyz", self.normal_axis) - colocate_coords = {} - for dim, sym in zip(plane_dims, self.solver_symmetry): - coords = grid_snapped.boundaries.to_dict[dim] - if len(coords) > 2: - if sym == 0: - colocate_coords[dim] = coords[1:-1] - else: - colocate_coords[dim] = coords[:-1] - # Colocate to new coordinates using the previously created data - data_dict_colocated = {} - for key, field in mode_solver_data.symmetry_expanded_copy.field_components.items(): - data_dict_colocated[key] = field.interp(**colocate_coords) - # Update data - mode_solver_monitor = self.to_mode_solver_monitor(name=MODE_MONITOR_NAME) - grid_expanded = self.simulation.discretize_monitor(mode_solver_monitor) - mode_solver_data = mode_solver_data.updated_copy( - monitor=mode_solver_monitor, grid_expanded=grid_expanded, **data_dict_colocated - ) + return mode_solver_data - # normalize modes - scaling = np.sqrt(np.abs(mode_solver_data.flux)) - mode_solver_data = mode_solver_data.copy( - update={ - key: field / scaling for key, field in mode_solver_data.field_components.items() - } - ) + def _colocate_data(self, mode_solver_data: ModeSolverData) -> ModeSolverData: + """Colocate data to Yee grid boundaries.""" + + # Get colocation coordinates in the solver plane + _, plane_dims = self.plane.pop_axis("xyz", self.normal_axis) + colocate_coords = {} + for dim, sym in zip(plane_dims, self.solver_symmetry): + coords = self.grid_snapped.boundaries.to_dict[dim] + if len(coords) > 2: + if sym == 0: + colocate_coords[dim] = coords[1:-1] + else: + colocate_coords[dim] = coords[:-1] + + # Colocate input data to new coordinates + data_dict_colocated = {} + for key, field in mode_solver_data.symmetry_expanded.field_components.items(): + data_dict_colocated[key] = field.interp(**colocate_coords).astype(field.dtype) + + # Update data + mode_solver_monitor = self.to_mode_solver_monitor(name=MODE_MONITOR_NAME) + grid_expanded = self.simulation.discretize_monitor(mode_solver_monitor) + data_dict_colocated.update({"monitor": mode_solver_monitor, "grid_expanded": grid_expanded}) + mode_solver_data = mode_solver_data._updated(update=data_dict_colocated) - # filter polarization if requested - if self.mode_spec.filter_pol is not None: - pol_frac = mode_solver_data.pol_fraction - for ifreq in range(len(self.freqs)): - te_frac = pol_frac.te.isel(f=ifreq) - if self.mode_spec.filter_pol == "te": - sort_inds = np.concatenate( - (np.where(te_frac >= 0.5)[0], np.where(te_frac < 0.5)[0]) + return mode_solver_data + + def _normalize_modes(self, mode_solver_data: ModeSolverData): + """Normalize modes. Note: this modifies ``mode_solver_data`` in-place.""" + scaling = np.sqrt(np.abs(mode_solver_data.flux)) + for field in mode_solver_data.field_components.values(): + field /= scaling + + def _filter_polarization(self, mode_solver_data: ModeSolverData): + """Filter polarization. Note: this modifies ``mode_solver_data`` in-place.""" + pol_frac = mode_solver_data.pol_fraction + for ifreq in range(len(self.freqs)): + te_frac = pol_frac.te.isel(f=ifreq) + if self.mode_spec.filter_pol == "te": + sort_inds = np.concatenate( + ( + np.where(te_frac >= 0.5)[0], + np.where(te_frac < 0.5)[0], + np.where(np.isnan(te_frac))[0], ) - elif self.mode_spec.filter_pol == "tm": - sort_inds = np.concatenate( - (np.where(te_frac <= 0.5)[0], np.where(te_frac > 0.5)[0]) + ) + elif self.mode_spec.filter_pol == "tm": + sort_inds = np.concatenate( + ( + np.where(te_frac <= 0.5)[0], + np.where(te_frac > 0.5)[0], + np.where(np.isnan(te_frac))[0], ) - for data in list(mode_solver_data.field_components.values()) + [ - mode_solver_data.n_complex, - mode_solver_data.grid_primal_correction, - mode_solver_data.grid_dual_correction, - ]: - data.values[..., ifreq, :] = data.values[..., ifreq, sort_inds] - - # sort modes if requested - if self.mode_spec.track_freq and len(self.freqs) > 1: - mode_solver_data = mode_solver_data.overlap_sort(self.mode_spec.track_freq) - - self._field_decay_warning(mode_solver_data.symmetry_expanded_copy) - - return mode_solver_data + ) + for data in list(mode_solver_data.field_components.values()) + [ + mode_solver_data.n_complex, + mode_solver_data.grid_primal_correction, + mode_solver_data.grid_dual_correction, + ]: + data.values[..., ifreq, :] = data.values[..., ifreq, sort_inds] @cached_property def data(self) -> ModeSolverData: @@ -403,7 +450,6 @@ def _solve_all_freqs( n_complex = [] eps_spec = [] for freq in self.freqs: - n_freq, fields_freq, eps_spec_freq = self._solve_single_freq( freq=freq, coords=coords, symmetry=symmetry ) @@ -423,6 +469,10 @@ def _solve_single_freq( The fields are rotated from propagation coordinates back to global coordinates. """ + + if not LOCAL_SOLVER_IMPORTED: + raise ImportError(IMPORT_ERROR_MSG) + solver_fields, n_complex, eps_spec = compute_modes( eps_cross=self._solver_eps(freq), coords=coords, @@ -564,10 +614,51 @@ def _grid_correction( return FreqModeDataArray(phase_primal), FreqModeDataArray(phase_dual) + @property + def _is_tensorial(self) -> bool: + """Whether the mode computation should be fully tensorial. This is either due to fully + anisotropic media, or due to an angled waveguide, in which case the transformed eps and mu + become tensorial. A separate check is done inside the solver, which looks at the actual + eps and mu and uses a tolerance to determine whether to invoke the tensorial solver, so + the actual behavior may differ from what's predicted by this property.""" + return abs(self.mode_spec.angle_theta) > 0 or self._has_fully_anisotropic_media + + @cached_property + def _intersecting_media(self) -> List: + """List of media (including simulation background) intersecting the mode plane.""" + total_structures = [self.simulation.scene.background_structure] + total_structures += list(self.simulation.structures) + return self.simulation.scene.intersecting_media(self.plane, total_structures) + + @cached_property + def _has_fully_anisotropic_media(self) -> bool: + """Check if there are any fully anisotropic media in the plane of the mode.""" + if np.any( + [isinstance(mat, FullyAnisotropicMedium) for mat in self.simulation.scene.mediums] + ): + for int_mat in self._intersecting_media: + if isinstance(int_mat, FullyAnisotropicMedium): + return True + return False + + @cached_property + def _has_complex_eps(self) -> bool: + """Check if there are media with a complex-valued epsilon in the plane of the mode. + A separate check is done inside the solver, which looks at the actual + eps and mu and uses a tolerance to determine whether to use real or complex fields, so + the actual behavior may differ from what's predicted by this property.""" + check_freqs = np.unique([np.amin(self.freqs), np.amax(self.freqs), np.mean(self.freqs)]) + for int_mat in self._intersecting_media: + for freq in check_freqs: + max_imag_eps = np.amax(np.abs(np.imag(int_mat.eps_model(freq)))) + if not isclose(max_imag_eps, 0): + return False + return True + def to_source( self, source_time: SourceTime, - direction: Direction, + direction: Direction = None, mode_index: pydantic.NonNegativeInt = 0, ) -> ModeSource: """Creates :class:`.ModeSource` from a :class:`ModeSolver` instance plus additional @@ -577,8 +668,9 @@ def to_source( ---------- source_time: :class:`.SourceTime` Specification of the source time-dependence. - direction : Direction + direction : Direction = None Whether source will inject in ``"+"`` or ``"-"`` direction relative to plane normal. + If not specified, uses the direction from the mode solver. mode_index : int = 0 Index into the list of modes returned by mode solver to use in source. @@ -589,6 +681,9 @@ def to_source( inputs. """ + if direction is None: + direction = self.direction + return ModeSource( center=self.plane.center, size=self.plane.size, @@ -598,7 +693,7 @@ def to_source( direction=direction, ) - def to_monitor(self, freqs: List[float], name: str) -> ModeMonitor: + def to_monitor(self, freqs: List[float] = None, name: str = None) -> ModeMonitor: """Creates :class:`ModeMonitor` from a :class:`ModeSolver` instance plus additional specifications. @@ -606,6 +701,7 @@ def to_monitor(self, freqs: List[float], name: str) -> ModeMonitor: ---------- freqs : List[float] Frequencies to include in Monitor (Hz). + If not specified, passes ``self.freqs``. name : str Required name of monitor. @@ -616,6 +712,15 @@ def to_monitor(self, freqs: List[float], name: str) -> ModeMonitor: inputs. """ + if freqs is None: + freqs = self.freqs + + if name is None: + raise ValueError( + "A 'name' must be passed to 'ModeSolver.to_monitor'. " + "The default value of 'None' is for backwards compatibility and is not accepted." + ) + return ModeMonitor( center=self.plane.center, size=self.plane.size, @@ -657,7 +762,7 @@ def to_mode_solver_monitor(self, name: str, colocate: bool = None) -> ModeSolver def sim_with_source( self, source_time: SourceTime, - direction: Direction, + direction: Direction = None, mode_index: pydantic.NonNegativeInt = 0, ) -> Simulation: """Creates :class:`Simulation` from a :class:`ModeSolver`. Creates a copy of @@ -668,8 +773,9 @@ def sim_with_source( ---------- source_time: :class:`.SourceTime` Specification of the source time-dependence. - direction : Direction + direction : Direction = None Whether source will inject in ``"+"`` or ``"-"`` direction relative to plane normal. + If not specified, uses the direction from the mode solver. mode_index : int = 0 Index into the list of modes returned by mode solver to use in source. @@ -679,6 +785,7 @@ def sim_with_source( Copy of the simulation with a :class:`.ModeSource` with specifications taken from the ModeSolver instance and the method inputs. """ + mode_source = self.to_source( mode_index=mode_index, direction=direction, source_time=source_time ) @@ -688,8 +795,8 @@ def sim_with_source( def sim_with_monitor( self, - freqs: List[float], - name: str, + freqs: List[float] = None, + name: str = None, ) -> Simulation: """Creates :class:`.Simulation` from a :class:`ModeSolver`. Creates a copy of the ModeSolver's original simulation with a mode monitor added corresponding to @@ -697,8 +804,9 @@ def sim_with_monitor( Parameters ---------- - freqs : List[float] + freqs : List[float] = None Frequencies to include in Monitor (Hz). + If not specified, uses the frequencies from the mode solver. name : str Required name of monitor. @@ -708,6 +816,7 @@ def sim_with_monitor( Copy of the simulation with a :class:`.ModeMonitor` with specifications taken from the ModeSolver instance and the method inputs. """ + mode_monitor = self.to_monitor(freqs=freqs, name=name) new_monitors = list(self.simulation.monitors) + [mode_monitor] new_sim = self.simulation.updated_copy(monitors=new_monitors) @@ -800,3 +909,20 @@ def plot_field( ax=ax, **sel_kwargs, ) + + def _validate_modes_size(self): + """Make sure that the total size of the modes fields is not too large.""" + monitor = self.to_mode_solver_monitor(name=MODE_MONITOR_NAME) + num_cells = self.simulation._monitor_num_cells(monitor) + # size in GB + total_size = monitor._storage_size_solver(num_cells=num_cells, tmesh=[]) / 1e9 + if total_size > MAX_MODES_DATA_SIZE_GB: + raise SetupError( + f"Mode solver has {total_size:.2f}GB of estimated storage, " + f"a maximum of {MAX_MODES_DATA_SIZE_GB:.2f}GB is allowed. Consider making the " + "mode plane smaller, or decreasing the resolution or number of requested " + "frequencies or modes." + ) + + def validate_pre_upload(self, source_required: bool = True): + self._validate_modes_size() diff --git a/tidy3d/plugins/mode/solver.py b/tidy3d/plugins/mode/solver.py index dea55b6cc..4e68f0d75 100644 --- a/tidy3d/plugins/mode/solver.py +++ b/tidy3d/plugins/mode/solver.py @@ -198,6 +198,10 @@ def compute_modes( fields = np.stack((E, H), axis=0) + if mode_spec.precision == "single": + # Recast to single precision which may have changed due to earlier manipulations + fields = fields.astype(np.complex64) + return fields, neff + 1j * keff, eps_spec @classmethod diff --git a/tidy3d/plugins/mode/web.py b/tidy3d/plugins/mode/web.py index 1504cdbac..cc132832c 100644 --- a/tidy3d/plugins/mode/web.py +++ b/tidy3d/plugins/mode/web.py @@ -1,470 +1,4 @@ """Web API for mode solver""" +from ...web.api.mode import run -from __future__ import annotations -from typing import Optional, Callable - -from datetime import datetime -import os -import pathlib -import tempfile -import time - -import pydantic.v1 as pydantic - -from ...components.simulation import Simulation -from ...components.data.monitor_data import ModeSolverData -from ...exceptions import WebError -from ...log import log, get_logging_console -from ...web.http_management import http -from ...web.s3utils import download_file, upload_file -from ...web.simulation_task import Folder, SIMULATION_JSON, SIM_FILE_HDF5_GZ -from ...web.types import ResourceLifecycle, Submittable - -from .mode_solver import ModeSolver, MODE_MONITOR_NAME -from ...version import __version__ - -MODESOLVER_API = "tidy3d/modesolver/py" -MODESOLVER_JSON = "mode_solver.json" -MODESOLVER_HDF5 = "mode_solver.hdf5" -MODESOLVER_GZ = "mode_solver.hdf5.gz" - -MODESOLVER_LOG = "output/result.log" -MODESOLVER_RESULT = "output/result.hdf5" - - -def run( - mode_solver: ModeSolver, - task_name: str = "Untitled", - mode_solver_name: str = "mode_solver", - folder_name: str = "Mode Solver", - results_file: str = "mode_solver.hdf5", - verbose: bool = True, - progress_callback_upload: Callable[[float], None] = None, - progress_callback_download: Callable[[float], None] = None, -) -> ModeSolverData: - """Submits a :class:`.ModeSolver` to server, starts running, monitors progress, downloads, - and loads results as a :class:`.ModeSolverData` object. - - Parameters - ---------- - mode_solver : :class:`.ModeSolver` - Mode solver to upload to server. - task_name : str = "Untitled" - Name of task. - mode_solver_name: str = "mode_solver" - The name of the mode solver to create the in task. - folder_name : str = "Mode Solver" - Name of folder to store task on web UI. - results_file : str = "mode_solver.hdf5" - Path to download results file (.hdf5). - verbose : bool = True - If `True`, will print status, otherwise, will run silently. - progress_callback_upload : Callable[[float], None] = None - Optional callback function called when uploading file with ``bytes_in_chunk`` as argument. - progress_callback_download : Callable[[float], None] = None - Optional callback function called when downloading file with ``bytes_in_chunk`` as argument. - - Returns - ------- - :class:`.ModeSolverData` - Mode solver data with the calculated results. - """ - - log_level = "DEBUG" if verbose else "INFO" - if verbose: - console = get_logging_console() - - task = ModeSolverTask.create(mode_solver, task_name, mode_solver_name, folder_name) - if verbose: - console.log( - f"Mode solver created with task_id='{task.task_id}', solver_id='{task.solver_id}'." - ) - task.upload(verbose=verbose, progress_callback=progress_callback_upload) - task.submit() - - # Wait for task to finish - prev_status = "draft" - status = task.status - while status not in ("success", "error", "diverged", "deleted"): - if status != prev_status: - log.log(log_level, f"Mode solver status: {status}") - if verbose: - console.log(f"Mode solver status: {status}") - prev_status = status - time.sleep(0.5) - status = task.get_info().status - - if status == "error": - raise WebError("Error running mode solver.") - - log.log(log_level, f"Mode solver status: {status}") - if verbose: - console.log(f"Mode solver status: {status}") - - if status != "success": - # Our cache discards None, so the user is able to re-run - return None - - return task.get_result( - to_file=results_file, verbose=verbose, progress_callback=progress_callback_download - ) - - -class ModeSolverTask(ResourceLifecycle, Submittable, extra=pydantic.Extra.allow): - """Interface for managing the running of a :class:`.ModeSolver` task on server.""" - - task_id: str = pydantic.Field( - None, - title="task_id", - description="Task ID number, set when the task is created, leave as None.", - alias="refId", - ) - - solver_id: str = pydantic.Field( - None, - title="solver", - description="Solver ID number, set when the task is created, leave as None.", - alias="id", - ) - - real_flex_unit: float = pydantic.Field( - None, title="real FlexCredits", description="Billed FlexCredits.", alias="charge" - ) - - created_at: Optional[datetime] = pydantic.Field( - title="created_at", description="Time at which this task was created.", alias="createdAt" - ) - - status: str = pydantic.Field( - None, - title="status", - description="Mode solver task status.", - ) - - file_type: str = pydantic.Field( - None, - title="file_type", - description="File type used to upload the mode solver.", - alias="fileType", - ) - - mode_solver: ModeSolver = pydantic.Field( - None, - title="mode_solver", - description="Mode solver being run by this task.", - ) - - @classmethod - def create( - cls, - mode_solver: ModeSolver, - task_name: str = "Untitled", - mode_solver_name: str = "mode_solver", - folder_name: str = "Mode Solver", - ) -> ModeSolverTask: - """Create a new mode solver task on the server. - - Parameters - ---------- - mode_solver: :class".ModeSolver" - The object that will be uploaded to server in the submitting phase. - task_name: str = "Untitled" - The name of the task. - mode_solver_name: str = "mode_solver" - The name of the mode solver to create the in task. - folder_name: str = "Mode Solver" - The name of the folder to store the task. - - Returns - ------- - :class:`ModeSolverTask` - :class:`ModeSolverTask` object containing information about the created task. - """ - folder = Folder.get(folder_name, create=True) - - mode_solver.simulation.validate_pre_upload(source_required=False) - resp = http.post( - MODESOLVER_API, - { - "projectId": folder.folder_id, - "taskName": task_name, - "protocolVersion": __version__, - "modeSolverName": mode_solver_name, - "fileType": "Gz", - "source": "Python", - }, - ) - log.info( - "Mode solver created with task_id='%s', solver_id='%s'.", resp["refId"], resp["id"] - ) - return ModeSolverTask(**resp, mode_solver=mode_solver) - - @classmethod - def get( - cls, - task_id: str, - solver_id: str, - to_file: str = "mode_solver.hdf5", - sim_file: str = "simulation.hdf5", - verbose: bool = True, - progress_callback: Callable[[float], None] = None, - ) -> ModeSolverTask: - """Get mode solver task from the server by id. - - Parameters - ---------- - task_id: str - Unique identifier of the task on server. - solver_id: str - Unique identifier of the mode solver in the task. - to_file: str = "mode_solver.hdf5" - File to store the mode solver downloaded from the task. - sim_file: str = "simulation.hdf5" - File to store the simulation downloaded from the task. - verbose: bool = True - Whether to display progress bars. - progress_callback : Callable[[float], None] = None - Optional callback function called while downloading the data. - - Returns - ------- - :class:`ModeSolverTask` - :class:`ModeSolverTask` object containing information about the task. - """ - resp = http.get(f"{MODESOLVER_API}/{task_id}/{solver_id}") - task = ModeSolverTask(**resp) - mode_solver = task.get_modesolver(to_file, sim_file, verbose, progress_callback) - return task.copy(update={"mode_solver": mode_solver}) - - def get_info(self) -> ModeSolverTask: - """Get the current state of this task on the server. - - Returns - ------- - :class:`ModeSolverTask` - :class:`ModeSolverTask` object containing information about the task, without the mode - solver. - """ - resp = http.get(f"{MODESOLVER_API}/{self.task_id}/{self.solver_id}") - return ModeSolverTask(**resp, mode_solver=self.mode_solver) - - def upload( - self, verbose: bool = True, progress_callback: Callable[[float], None] = None - ) -> None: - """Upload this task's 'mode_solver' to the server. - - Parameters - ---------- - verbose: bool = True - Whether to display progress bars. - progress_callback : Callable[[float], None] = None - Optional callback function called while uploading the data. - """ - mode_solver = self.mode_solver.copy() - - sim = mode_solver.simulation - - # Upload simulation.hdf5.gz for GUI display - file, file_name = tempfile.mkstemp(".hdf5.gz") - os.close(file) - try: - sim.to_hdf5_gz(file_name) - upload_file( - self.task_id, - file_name, - SIM_FILE_HDF5_GZ, - verbose=verbose, - progress_callback=progress_callback, - ) - finally: - os.unlink(file_name) - - # Upload a single HDF5 file with the full data - file, file_name = tempfile.mkstemp(".hdf5.gz") - os.close(file) - try: - mode_solver.to_hdf5_gz(file_name) - upload_file( - self.solver_id, - file_name, - MODESOLVER_GZ, - verbose=verbose, - progress_callback=progress_callback, - ) - finally: - os.unlink(file_name) - - def submit(self): - """Start the execution of this task. - - The mode solver must be uploaded to the server with the :meth:`ModeSolverTask.upload` method - before this step. - """ - http.post(f"{MODESOLVER_API}/{self.task_id}/{self.solver_id}/run") - - def delete(self): - """Delete the mode solver and its corresponding task from the server.""" - # Delete mode solver - http.delete(f"{MODESOLVER_API}/{self.task_id}/{self.solver_id}") - # Delete parent task - http.delete(f"tidy3d/tasks/{self.task_id}") - - def abort(self): - """Abort the mode solver and its corresponding task from the server.""" - return http.put( - "tidy3d/tasks/abort", json={"taskType": "MODE_SOLVER", "taskId": self.solver_id} - ) - - def get_modesolver( - self, - to_file: str = "mode_solver.hdf5", - sim_file: str = "simulation.hdf5", - verbose: bool = True, - progress_callback: Callable[[float], None] = None, - ) -> ModeSolver: - """Get mode solver associated with this task from the server. - - Parameters - ---------- - to_file: str = "mode_solver.hdf5" - File to store the mode solver downloaded from the task. - sim_file: str = "simulation.hdf5" - File to store the simulation downloaded from the task, if any. - verbose: bool = True - Whether to display progress bars. - progress_callback : Callable[[float], None] = None - Optional callback function called while downloading the data. - - Returns - ------- - :class:`ModeSolver` - :class:`ModeSolver` object associated with this task. - """ - if self.file_type == "Gz": - file, file_path = tempfile.mkstemp(".hdf5.gz") - os.close(file) - try: - download_file( - self.solver_id, - MODESOLVER_GZ, - to_file=file_path, - verbose=verbose, - progress_callback=progress_callback, - ) - mode_solver = ModeSolver.from_hdf5_gz(file_path) - finally: - os.unlink(file_path) - - elif self.file_type == "Hdf5": - file, file_path = tempfile.mkstemp(".hdf5") - os.close(file) - try: - download_file( - self.solver_id, - MODESOLVER_HDF5, - to_file=file_path, - verbose=verbose, - progress_callback=progress_callback, - ) - mode_solver = ModeSolver.from_hdf5(file_path) - finally: - os.unlink(file_path) - - else: - file, file_path = tempfile.mkstemp(".json") - os.close(file) - try: - download_file( - self.solver_id, - MODESOLVER_JSON, - to_file=file_path, - verbose=verbose, - progress_callback=progress_callback, - ) - mode_solver_dict = ModeSolver.dict_from_json(file_path) - finally: - os.unlink(file_path) - - download_file( - self.task_id, - SIMULATION_JSON, - to_file=sim_file, - verbose=verbose, - progress_callback=progress_callback, - ) - mode_solver_dict["simulation"] = Simulation.from_json(sim_file) - mode_solver = ModeSolver.parse_obj(mode_solver_dict) - - # Store requested mode solver file - mode_solver.to_file(to_file) - - return mode_solver - - def get_result( - self, - to_file: str = "mode_solver_data.hdf5", - verbose: bool = True, - progress_callback: Callable[[float], None] = None, - ) -> ModeSolverData: - """Get mode solver results for this task from the server. - - Parameters - ---------- - to_file: str = "mode_solver_data.hdf5" - File to store the mode solver downloaded from the task. - verbose: bool = True - Whether to display progress bars. - progress_callback : Callable[[float], None] = None - Optional callback function called while downloading the data. - - Returns - ------- - :class:`.ModeSolverData` - Mode solver data with the calculated results. - """ - download_file( - self.solver_id, - MODESOLVER_RESULT, - to_file=to_file, - verbose=verbose, - progress_callback=progress_callback, - ) - data = ModeSolverData.from_hdf5(to_file) - data = data.copy( - update={"monitor": self.mode_solver.to_mode_solver_monitor(name=MODE_MONITOR_NAME)} - ) - - self.mode_solver._cached_properties["data_raw"] = data - - # Perform symmetry expansion - return self.mode_solver.data - - def get_log( - self, - to_file: str = "mode_solver.log", - verbose: bool = True, - progress_callback: Callable[[float], None] = None, - ) -> pathlib.Path: - """Get execution log for this task from the server. - - Parameters - ---------- - to_file: str = "mode_solver.log" - File to store the mode solver downloaded from the task. - verbose: bool = True - Whether to display progress bars. - progress_callback : Callable[[float], None] = None - Optional callback function called while downloading the data. - - Returns - ------- - path: pathlib.Path - Path to saved file. - """ - return download_file( - self.solver_id, - MODESOLVER_LOG, - to_file=to_file, - verbose=verbose, - progress_callback=progress_callback, - ) +__all__ = ["run"] diff --git a/tidy3d/plugins/smatrix/smatrix.py b/tidy3d/plugins/smatrix/smatrix.py index f0cddcb18..ad98aa2de 100644 --- a/tidy3d/plugins/smatrix/smatrix.py +++ b/tidy3d/plugins/smatrix/smatrix.py @@ -20,7 +20,7 @@ from ...components.base import Tidy3dBaseModel, cached_property from ...exceptions import SetupError, Tidy3dKeyError from ...log import log -from ...web.container import BatchData, Batch +from ...web.api.container import BatchData, Batch # fwidth of gaussian pulse in units of central frequency FWIDTH_FRAC = 1.0 / 10 @@ -298,7 +298,9 @@ def _task_name(port: Port, mode_index: int) -> str: @equal_aspect @add_ax_if_none - def plot_sim(self, x: float = None, y: float = None, z: float = None, ax: Ax = None) -> Ax: + def plot_sim( + self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs + ) -> Ax: """Plot a :class:`Simulation` with all sources added for each port, for troubleshooting.""" plot_sources = [] @@ -306,7 +308,21 @@ def plot_sim(self, x: float = None, y: float = None, z: float = None, ax: Ax = N mode_source_0 = self.to_source(port=port_source, mode_index=0) plot_sources.append(mode_source_0) sim_plot = self.simulation.copy(update=dict(sources=plot_sources)) - return sim_plot.plot(x=x, y=y, z=z, ax=ax) + return sim_plot.plot(x=x, y=y, z=z, ax=ax, **kwargs) + + @equal_aspect + @add_ax_if_none + def plot_sim_eps( + self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs + ) -> Ax: + """Plot permittivity of the :class:`Simulation` with all sources added for each port.""" + + plot_sources = [] + for port_source in self.ports: + mode_source_0 = self.to_source(port=port_source, mode_index=0) + plot_sources.append(mode_source_0) + sim_plot = self.simulation.copy(update=dict(sources=plot_sources)) + return sim_plot.plot_eps(x=x, y=y, z=z, ax=ax, **kwargs) @cached_property def batch(self) -> Batch: @@ -349,7 +365,8 @@ def get_path_dir(self, path_dir: str) -> None: @cached_property def _batch_path(self) -> str: """Where we store the batch for this ComponentModeler instance after the run.""" - return os.path.join(self.path_dir, "batch" + str(hash(self)) + ".json") + hash_str = self._hash_self() + return os.path.join(self.path_dir, "batch" + hash_str + ".json") def _run_sims(self, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: """Run :class:`Simulations` for each port and return the batch after saving.""" diff --git a/tidy3d/schema.json b/tidy3d/schema.json index b5b8d600e..8eaddf9bd 100644 --- a/tidy3d/schema.json +++ b/tidy3d/schema.json @@ -1,6 +1,6 @@ { "title": "Simulation", - "description": "Contains all information about Tidy3d simulation.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nrun_time : PositiveFloat\n [units = sec]. Total electromagnetic evolution time in seconds. Note: If simulation 'shutoff' is specified, simulation will terminate early when shutoff condition met. \nmedium : Union[Medium, AnisotropicMedium, PECMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, FullyAnisotropicMedium, CustomMedium, CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomAnisotropicMedium, PerturbationMedium, PerturbationPoleResidue] = Medium(name=None, frequency_range=None, allow_gain=False, nonlinear_spec=None, type='Medium', permittivity=1.0, conductivity=0.0)\n Background medium of simulation, defaults to vacuum if not specified.\nsymmetry : Tuple[Literal[0, -1, 1], Literal[0, -1, 1], Literal[0, -1, 1]] = (0, 0, 0)\n Tuple of integers defining reflection symmetry across a plane bisecting the simulation domain normal to the x-, y-, and z-axis at the simulation center of each 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.\nstructures : Tuple[Structure, ...] = ()\n Tuple of structures present in simulation. Note: Structures defined later in this list override the simulation material properties in regions of spatial overlap.\nsources : Tuple[Annotated[Union[tidy3d.components.source.UniformCurrentSource, tidy3d.components.source.PointDipole, tidy3d.components.source.GaussianBeam, tidy3d.components.source.AstigmaticGaussianBeam, tidy3d.components.source.ModeSource, tidy3d.components.source.PlaneWave, tidy3d.components.source.CustomFieldSource, tidy3d.components.source.CustomCurrentSource, tidy3d.components.source.TFSF], FieldInfo(default=PydanticUndefined, discriminator='type', extra={})], ...] = ()\n Tuple of electric current sources injecting fields into the simulation.\nboundary_spec : BoundarySpec = BoundarySpec(x=Boundary(plus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, minus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, type='Boundary'), y=Boundary(plus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, minus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, type='Boundary'), z=Boundary(plus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, minus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, type='Boundary'), type='BoundarySpec')\n Specification of boundary conditions along each dimension. If ``None``, PML boundary conditions are applied on all sides.\nmonitors : Tuple[Annotated[Union[tidy3d.components.monitor.FieldMonitor, tidy3d.components.monitor.FieldTimeMonitor, tidy3d.components.monitor.PermittivityMonitor, tidy3d.components.monitor.FluxMonitor, tidy3d.components.monitor.FluxTimeMonitor, tidy3d.components.monitor.ModeMonitor, tidy3d.components.monitor.ModeSolverMonitor, tidy3d.components.monitor.FieldProjectionAngleMonitor, tidy3d.components.monitor.FieldProjectionCartesianMonitor, tidy3d.components.monitor.FieldProjectionKSpaceMonitor, tidy3d.components.monitor.DiffractionMonitor], FieldInfo(default=PydanticUndefined, discriminator='type', extra={})], ...] = ()\n Tuple of monitors in the simulation. Note: monitor names are used to access data after simulation is run.\ngrid_spec : GridSpec = GridSpec(grid_x=AutoGrid(type='AutoGrid',, min_steps_per_wvl=10.0,, max_scale=1.4,, dl_min=0.0,, mesher=GradedMesher(type='GradedMesher')), grid_y=AutoGrid(type='AutoGrid',, min_steps_per_wvl=10.0,, max_scale=1.4,, dl_min=0.0,, mesher=GradedMesher(type='GradedMesher')), grid_z=AutoGrid(type='AutoGrid',, min_steps_per_wvl=10.0,, max_scale=1.4,, dl_min=0.0,, mesher=GradedMesher(type='GradedMesher')), wavelength=None, override_structures=(), type='GridSpec')\n Specifications for the simulation grid along each of the three directions.\nshutoff : NonNegativeFloat = 1e-05\n Ratio of the instantaneous integrated E-field intensity to the maximum value at which the simulation will automatically terminate time stepping. Used to prevent extraneous run time of simulations with fully decayed fields. Set to ``0`` to disable this feature.\nsubpixel : bool = True\n If ``True``, uses subpixel averaging of the permittivity based on structure definition, resulting in much higher accuracy for a given grid size.\nnormalize_index : Optional[NonNegativeInt] = 0\n Index of the source in the tuple of sources whose spectrum will be used to normalize the frequency-dependent data. If ``None``, the raw field data is returned unnormalized.\ncourant : ConstrainedFloatValue = 0.99\n 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. This factor is normalized to no larger than 1 when CFL stability condition is met in 3D.\nversion : str = 2.4.3\n String specifying the front end version number.\n\nExample\n-------\n>>> from tidy3d import Sphere, Cylinder, PolySlab\n>>> from tidy3d import UniformCurrentSource, GaussianPulse\n>>> from tidy3d import FieldMonitor, FluxMonitor\n>>> from tidy3d import GridSpec, AutoGrid\n>>> from tidy3d import BoundarySpec, Boundary\n>>> sim = Simulation(\n... size=(3.0, 3.0, 3.0),\n... grid_spec=GridSpec(\n... grid_x = AutoGrid(min_steps_per_wvl = 20),\n... grid_y = AutoGrid(min_steps_per_wvl = 20),\n... grid_z = AutoGrid(min_steps_per_wvl = 20)\n... ),\n... run_time=40e-11,\n... structures=[\n... Structure(\n... geometry=Box(size=(1, 1, 1), center=(0, 0, 0)),\n... medium=Medium(permittivity=2.0),\n... ),\n... ],\n... sources=[\n... UniformCurrentSource(\n... size=(0, 0, 0),\n... center=(0, 0.5, 0),\n... polarization=\"Hx\",\n... source_time=GaussianPulse(\n... freq0=2e14,\n... fwidth=4e13,\n... ),\n... )\n... ],\n... monitors=[\n... FluxMonitor(size=(1, 1, 0), center=(0, 0, 0), freqs=[2e14, 2.5e14], name='flux'),\n... ],\n... symmetry=(0, 0, 0),\n... boundary_spec=BoundarySpec(\n... x = Boundary.pml(num_layers=20),\n... y = Boundary.pml(num_layers=30),\n... z = Boundary.periodic(),\n... ),\n... shutoff=1e-6,\n... courant=0.8,\n... subpixel=False,\n... )", + "description": "Contains all information about Tidy3d simulation.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nmedium : Union[Medium, AnisotropicMedium, PECMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, FullyAnisotropicMedium, CustomMedium, CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomAnisotropicMedium, PerturbationMedium, PerturbationPoleResidue] = Medium(name=None, frequency_range=None, allow_gain=False, nonlinear_spec=None, modulation_spec=None, heat_spec=None, type='Medium', permittivity=1.0, conductivity=0.0)\n Background medium of simulation, defaults to vacuum if not specified.\nstructures : Tuple[Structure, ...] = ()\n Tuple of structures present in simulation. Note: Structures defined later in this list override the simulation material properties in regions of spatial overlap.\nsymmetry : Tuple[Literal[0, -1, 1], Literal[0, -1, 1], Literal[0, -1, 1]] = (0, 0, 0)\n Tuple of integers defining reflection symmetry across a plane bisecting the simulation domain normal to the x-, y-, and z-axis at the simulation center of each 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.\nsources : Tuple[Annotated[Union[tidy3d.components.source.UniformCurrentSource, tidy3d.components.source.PointDipole, tidy3d.components.source.GaussianBeam, tidy3d.components.source.AstigmaticGaussianBeam, tidy3d.components.source.ModeSource, tidy3d.components.source.PlaneWave, tidy3d.components.source.CustomFieldSource, tidy3d.components.source.CustomCurrentSource, tidy3d.components.source.TFSF], FieldInfo(default=PydanticUndefined, discriminator='type', extra={})], ...] = ()\n Tuple of electric current sources injecting fields into the simulation.\nboundary_spec : BoundarySpec = BoundarySpec(x=Boundary(plus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, minus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, type='Boundary'), y=Boundary(plus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, minus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, type='Boundary'), z=Boundary(plus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, minus=PML(name=None,, type='PML',, num_layers=12,, parameters=PMLParams(sigma_order=3,, sigma_min=0.0,, sigma_max=1.5,, type='PMLParams',, kappa_order=3,, kappa_min=1.0,, kappa_max=3.0,, alpha_order=1,, alpha_min=0.0,, alpha_max=0.0)),, type='Boundary'), type='BoundarySpec')\n Specification of boundary conditions along each dimension. If ``None``, PML boundary conditions are applied on all sides.\nmonitors : Tuple[Annotated[Union[tidy3d.components.monitor.FieldMonitor, tidy3d.components.monitor.FieldTimeMonitor, tidy3d.components.monitor.PermittivityMonitor, tidy3d.components.monitor.FluxMonitor, tidy3d.components.monitor.FluxTimeMonitor, tidy3d.components.monitor.ModeMonitor, tidy3d.components.monitor.ModeSolverMonitor, tidy3d.components.monitor.FieldProjectionAngleMonitor, tidy3d.components.monitor.FieldProjectionCartesianMonitor, tidy3d.components.monitor.FieldProjectionKSpaceMonitor, tidy3d.components.monitor.DiffractionMonitor], FieldInfo(default=PydanticUndefined, discriminator='type', extra={})], ...] = ()\n Tuple of monitors in the simulation. Note: monitor names are used to access data after simulation is run.\ngrid_spec : GridSpec = GridSpec(grid_x=AutoGrid(type='AutoGrid',, min_steps_per_wvl=10.0,, max_scale=1.4,, dl_min=0.0,, mesher=GradedMesher(type='GradedMesher')), grid_y=AutoGrid(type='AutoGrid',, min_steps_per_wvl=10.0,, max_scale=1.4,, dl_min=0.0,, mesher=GradedMesher(type='GradedMesher')), grid_z=AutoGrid(type='AutoGrid',, min_steps_per_wvl=10.0,, max_scale=1.4,, dl_min=0.0,, mesher=GradedMesher(type='GradedMesher')), wavelength=None, override_structures=(), type='GridSpec')\n Specifications for the simulation grid along each of the three directions.\nversion : str = 2.5.0\n String specifying the front end version number.\nrun_time : PositiveFloat\n [units = sec]. Total electromagnetic evolution time in seconds. Note: If simulation 'shutoff' is specified, simulation will terminate early when shutoff condition met. \nshutoff : NonNegativeFloat = 1e-05\n Ratio of the instantaneous integrated E-field intensity to the maximum value at which the simulation will automatically terminate time stepping. Used to prevent extraneous run time of simulations with fully decayed fields. Set to ``0`` to disable this feature.\nsubpixel : bool = True\n If ``True``, uses subpixel averaging of the permittivity based on structure definition, resulting in much higher accuracy for a given grid size.\nnormalize_index : Optional[NonNegativeInt] = 0\n Index of the source in the tuple of sources whose spectrum will be used to normalize the frequency-dependent data. If ``None``, the raw field data is returned unnormalized.\ncourant : ConstrainedFloatValue = 0.99\n 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. This factor is normalized to no larger than 1 when CFL stability condition is met in 3D.\n\nExample\n-------\n>>> from tidy3d import Sphere, Cylinder, PolySlab\n>>> from tidy3d import UniformCurrentSource, GaussianPulse\n>>> from tidy3d import FieldMonitor, FluxMonitor\n>>> from tidy3d import GridSpec, AutoGrid\n>>> from tidy3d import BoundarySpec, Boundary\n>>> sim = Simulation(\n... size=(3.0, 3.0, 3.0),\n... grid_spec=GridSpec(\n... grid_x = AutoGrid(min_steps_per_wvl = 20),\n... grid_y = AutoGrid(min_steps_per_wvl = 20),\n... grid_z = AutoGrid(min_steps_per_wvl = 20)\n... ),\n... run_time=40e-11,\n... structures=[\n... Structure(\n... geometry=Box(size=(1, 1, 1), center=(0, 0, 0)),\n... medium=Medium(permittivity=2.0),\n... ),\n... ],\n... sources=[\n... UniformCurrentSource(\n... size=(0, 0, 0),\n... center=(0, 0.5, 0),\n... polarization=\"Hx\",\n... source_time=GaussianPulse(\n... freq0=2e14,\n... fwidth=4e13,\n... ),\n... )\n... ],\n... monitors=[\n... FluxMonitor(size=(1, 1, 0), center=(0, 0, 0), freqs=[2e14, 2.5e14], name='flux'),\n... ],\n... symmetry=(0, 0, 0),\n... boundary_spec=BoundarySpec(\n... x = Boundary.pml(num_layers=20),\n... y = Boundary.pml(num_layers=30),\n... z = Boundary.periodic(),\n... ),\n... shutoff=1e-6,\n... courant=0.8,\n... subpixel=False,\n... )", "type": "object", "properties": { "type": { @@ -57,13 +57,6 @@ } ] }, - "run_time": { - "title": "Run Time", - "description": "Total electromagnetic evolution time in seconds. Note: If simulation 'shutoff' is specified, simulation will terminate early when shutoff condition met. ", - "units": "sec", - "exclusiveMinimum": 0, - "type": "number" - }, "medium": { "title": "Background Medium", "description": "Background medium of simulation, defaults to vacuum if not specified.", @@ -72,6 +65,8 @@ "frequency_range": null, "allow_gain": false, "nonlinear_spec": null, + "modulation_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0 @@ -156,6 +151,15 @@ } ] }, + "structures": { + "title": "Structures", + "description": "Tuple of structures present in simulation. Note: Structures defined later in this list override the simulation material properties in regions of spatial overlap.", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Structure" + } + }, "symmetry": { "title": "Symmetries", "description": "Tuple of integers defining reflection symmetry across a plane bisecting the simulation domain normal to the x-, y-, and z-axis at the simulation center of each 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.", @@ -194,15 +198,6 @@ } ] }, - "structures": { - "title": "Structures", - "description": "Tuple of structures present in simulation. Note: Structures defined later in this list override the simulation material properties in regions of spatial overlap.", - "default": [], - "type": "array", - "items": { - "$ref": "#/definitions/Structure" - } - }, "sources": { "title": "Sources", "description": "Tuple of electric current sources injecting fields into the simulation.", @@ -477,6 +472,19 @@ } ] }, + "version": { + "title": "Version", + "description": "String specifying the front end version number.", + "default": "2.5.0", + "type": "string" + }, + "run_time": { + "title": "Run Time", + "description": "Total electromagnetic evolution time in seconds. Note: If simulation 'shutoff' is specified, simulation will terminate early when shutoff condition met. ", + "units": "sec", + "exclusiveMinimum": 0, + "type": "number" + }, "shutoff": { "title": "Shutoff Condition", "description": "Ratio of the instantaneous integrated E-field intensity to the maximum value at which the simulation will automatically terminate time stepping. Used to prevent extraneous run time of simulations with fully decayed fields. Set to ``0`` to disable this feature.", @@ -504,12 +512,6 @@ "exclusiveMinimum": 0.0, "maximum": 1.0, "type": "number" - }, - "version": { - "title": "Version", - "description": "String specifying the front end version number.", - "default": "2.4.3", - "type": "string" } }, "required": [ @@ -520,16 +522,9 @@ "definitions": { "NonlinearSusceptibility": { "title": "NonlinearSusceptibility", - "description": "Specification adding an instantaneous nonlinear susceptibility to a medium.\nThe expression for the instantaneous nonlinear polarization is given below.\n\nParameters\n----------\nnumiters : PositiveInt = 1\n Number of iterations for solving nonlinear constitutive relation.\nchi3 : float\n [units = um^2 / V^2]. Chi3 nonlinear susceptibility.\n\nNote\n----\n.. math::\n\n P_{NL} = \\epsilon_0 \\chi_3 |E|^2 E\n\nNote\n----\nThe nonlinear constitutive relation is solved iteratively; it may not converge\nfor strong nonlinearities. Increasing `numiters` can help with convergence.\n\nNote\n----\nFor complex fields (e.g. when using Bloch boundary conditions), the nonlinearity\nis applied separately to the real and imaginary parts, so that the above equation\nholds when both E and :math:`P_{NL}` are replaced by their real or imaginary parts.\nThe nonlinearity is only applied to the real-valued fields since they are the\nphysical fields.\n\nNote\n----\nDifferent field components do not interact nonlinearly. For example,\nwhen calculating :math:`P_{NL, x}`, we approximate :math:`|E|^2 \\approx |E_x|^2`.\nThis approximation is valid when the E field is predominantly polarized along one\nof the x, y, or z axes.\n\nExample\n-------\n>>> medium = Medium(permittivity=2, nonlinear_spec=NonlinearSusceptibility(chi3=1))", + "description": "Model for an instantaneous nonlinear chi3 susceptibility.\nThe expression for the instantaneous nonlinear polarization is given below.\n\nParameters\n----------\nchi3 : float = 0\n [units = um^2 / V^2]. Chi3 nonlinear susceptibility.\nnumiters : Optional[PositiveInt] = None\n Deprecated. The old usage 'nonlinear_spec=model' with 'model.numiters' is deprecated and will be removed in a future release. The new usage is 'nonlinear_spec=NonlinearSpec(models=\\[model], num_iters=num_iters)'. Under the new usage, this parameter is ignored, and 'NonlinearSpec.num_iters' is used instead.\n\nNote\n----\n.. math::\n\n P_{NL} = \\varepsilon_0 \\chi_3 |E|^2 E\n\nNote\n----\nThis model uses real time-domain fields, so :math:`\\chi_3` must be real.\nFor complex fields (e.g. when using Bloch boundary conditions), the nonlinearity\nis applied separately to the real and imaginary parts, so that the above equation\nholds when both E and :math:`P_{NL}` are replaced by their real or imaginary parts.\nThe nonlinearity is applied to the real and imaginary components separately since\neach of those represents a physical field.\n\nNote\n----\nDifferent field components do not interact nonlinearly. For example,\nwhen calculating :math:`P_{NL, x}`, we approximate :math:`|E|^2 \\approx |E_x|^2`.\nThis approximation is valid when the E field is predominantly polarized along one\nof the x, y, or z axes.\n\nExample\n-------\n>>> nonlinear_susceptibility = NonlinearSusceptibility(chi3=1)", "type": "object", "properties": { - "numiters": { - "title": "Number of iterations", - "description": "Number of iterations for solving nonlinear constitutive relation.", - "default": 1, - "exclusiveMinimum": 0, - "type": "integer" - }, "type": { "title": "Type", "default": "NonlinearSusceptibility", @@ -541,18 +536,471 @@ "chi3": { "title": "Chi3", "description": "Chi3 nonlinear susceptibility.", + "default": 0, "units": "um^2 / V^2", "type": "number" + }, + "numiters": { + "title": "Number of iterations", + "description": "Deprecated. The old usage 'nonlinear_spec=model' with 'model.numiters' is deprecated and will be removed in a future release. The new usage is 'nonlinear_spec=NonlinearSpec(models=\\[model], num_iters=num_iters)'. Under the new usage, this parameter is ignored, and 'NonlinearSpec.num_iters' is used instead.", + "exclusiveMinimum": 0, + "type": "integer" + } + }, + "additionalProperties": false + }, + "ComplexNumber": { + "title": "ComplexNumber", + "description": "Complex number with a well defined schema.", + "type": "object", + "properties": { + "real": { + "title": "Real", + "type": "number" + }, + "imag": { + "title": "Imag", + "type": "number" } }, "required": [ - "chi3" + "real", + "imag" + ] + }, + "TwoPhotonAbsorption": { + "title": "TwoPhotonAbsorption", + "description": "Model for two-photon absorption (TPA) nonlinearity which gives an intensity-dependent\nabsorption of the form :math:`\\alpha = \\alpha_0 + \\beta I`.\nThe expression for the nonlinear polarization is given below.\n\nParameters\n----------\nbeta : Union[tidycomplex, ComplexNumber] = 0\n [units = um / W]. Coefficient for two-photon absorption (TPA).\nn0 : Union[tidycomplex, ComplexNumber, NoneType] = None\n Complex linear refractive index of the medium, computed for instance using 'medium.nk_model'. If not provided, it is calculated automatically using the central frequencies of the simulation sources (as long as these are all equal).\n\nNote\n----\n.. math::\n\n P_{NL} = -\\frac{c_0^2 \\varepsilon_0^2 n_0 \\operatorname{Re}(n_0) \\beta}{2 i \\omega} |E|^2 E\n\nNote\n----\nThis frequency-domain equation is implemented in the time domain using complex-valued fields.\n\nNote\n----\nDifferent field components do not interact nonlinearly. For example,\nwhen calculating :math:`P_{NL, x}`, we approximate :math:`|E|^2 \\approx |E_x|^2`.\nThis approximation is valid when the E field is predominantly polarized along one\nof the x, y, or z axes.\n\nNote\n----\nThe implementation is described in::\n\n N. Suzuki, \"FDTD Analysis of Two-Photon Absorption and Free-Carrier Absorption in Si\n High-Index-Contrast Waveguides,\" J. Light. Technol. 25, 9 (2007).\n\nExample\n-------\n>>> tpa_model = TwoPhotonAbsorption(beta=1)", + "type": "object", + "properties": { + "type": { + "title": "Type", + "default": "TwoPhotonAbsorption", + "enum": [ + "TwoPhotonAbsorption" + ], + "type": "string" + }, + "beta": { + "title": "TPA coefficient", + "description": "Coefficient for two-photon absorption (TPA).", + "default": 0, + "units": "um / W", + "anyOf": [ + { + "title": "ComplexNumber", + "description": "Complex number with a well defined schema.", + "type": "object", + "properties": { + "real": { + "title": "Real", + "type": "number" + }, + "imag": { + "title": "Imag", + "type": "number" + } + }, + "required": [ + "real", + "imag" + ] + }, + { + "$ref": "#/definitions/ComplexNumber" + } + ] + }, + "n0": { + "title": "Complex linear refractive index", + "description": "Complex linear refractive index of the medium, computed for instance using 'medium.nk_model'. If not provided, it is calculated automatically using the central frequencies of the simulation sources (as long as these are all equal).", + "anyOf": [ + { + "title": "ComplexNumber", + "description": "Complex number with a well defined schema.", + "type": "object", + "properties": { + "real": { + "title": "Real", + "type": "number" + }, + "imag": { + "title": "Imag", + "type": "number" + } + }, + "required": [ + "real", + "imag" + ] + }, + { + "$ref": "#/definitions/ComplexNumber" + } + ] + } + }, + "additionalProperties": false + }, + "KerrNonlinearity": { + "title": "KerrNonlinearity", + "description": "Model for Kerr nonlinearity which gives an intensity-dependent refractive index\nof the form :math:`n = n_0 + n_2 I`. The expression for the nonlinear polarization\nis given below.\n\nParameters\n----------\nn2 : Union[tidycomplex, ComplexNumber] = 0\n [units = um^2 / W]. Nonlinear refractive index in the Kerr nonlinearity.\nn0 : Union[tidycomplex, ComplexNumber, NoneType] = None\n Complex linear refractive index of the medium, computed for instance using 'medium.nk_model'. If not provided, it is calculated automatically using the central frequencies of the simulation sources (as long as these are all equal).\n\nNote\n----\n.. math::\n\n P_{NL} = \\varepsilon_0 c_0 n_0 \\operatorname{Re}(n_0) n_2 |E|^2 E\n\nNote\n----\nThe fields in this equation are complex-valued, allowing a direct implementation of the Kerr\nnonlinearity. In contrast, the model :class:`.NonlinearSusceptibility` implements a\nchi3 nonlinear susceptibility using real-valued fields, giving rise to Kerr nonlinearity\nas well as third-harmonic generation. The relationship between the parameters is given by\n:math:`n_2 = \\frac{3}{4} \\frac{1}{\\varepsilon_0 c_0 n_0 \\operatorname{Re}(n_0)} \\chi_3`. The additional\nfactor of :math:`\\frac{3}{4}` comes from the usage of complex-valued fields for the Kerr\nnonlinearity and real-valued fields for the nonlinear susceptibility.\n\nNote\n----\nDifferent field components do not interact nonlinearly. For example,\nwhen calculating :math:`P_{NL, x}`, we approximate :math:`|E|^2 \\approx |E_x|^2`.\nThis approximation is valid when the E field is predominantly polarized along one\nof the x, y, or z axes.\n\nExample\n-------\n>>> kerr_model = KerrNonlinearity(n2=1)", + "type": "object", + "properties": { + "type": { + "title": "Type", + "default": "KerrNonlinearity", + "enum": [ + "KerrNonlinearity" + ], + "type": "string" + }, + "n2": { + "title": "Nonlinear refractive index", + "description": "Nonlinear refractive index in the Kerr nonlinearity.", + "default": 0, + "units": "um^2 / W", + "anyOf": [ + { + "title": "ComplexNumber", + "description": "Complex number with a well defined schema.", + "type": "object", + "properties": { + "real": { + "title": "Real", + "type": "number" + }, + "imag": { + "title": "Imag", + "type": "number" + } + }, + "required": [ + "real", + "imag" + ] + }, + { + "$ref": "#/definitions/ComplexNumber" + } + ] + }, + "n0": { + "title": "Complex linear refractive index", + "description": "Complex linear refractive index of the medium, computed for instance using 'medium.nk_model'. If not provided, it is calculated automatically using the central frequencies of the simulation sources (as long as these are all equal).", + "anyOf": [ + { + "title": "ComplexNumber", + "description": "Complex number with a well defined schema.", + "type": "object", + "properties": { + "real": { + "title": "Real", + "type": "number" + }, + "imag": { + "title": "Imag", + "type": "number" + } + }, + "required": [ + "real", + "imag" + ] + }, + { + "$ref": "#/definitions/ComplexNumber" + } + ] + } + }, + "additionalProperties": false + }, + "NonlinearSpec": { + "title": "NonlinearSpec", + "description": "Abstract specification for adding nonlinearities to a medium.\n\nParameters\n----------\nmodels : Tuple[Union[NonlinearSusceptibility, TwoPhotonAbsorption, KerrNonlinearity], ...] = ()\n The nonlinear models present in this nonlinear spec. Nonlinear models of different types are additive. Multiple nonlinear models of the same type are not allowed.\nnum_iters : PositiveInt = 5\n Number of iterations for solving nonlinear constitutive relation.\n\nNote\n----\nThe nonlinear constitutive relation is solved iteratively; it may not converge\nfor strong nonlinearities. Increasing ``num_iters`` can help with convergence.\n\nExample\n-------\n>>> nonlinear_susceptibility = NonlinearSusceptibility(chi3=1)\n>>> nonlinear_spec = NonlinearSpec(models=[nonlinear_susceptibility])\n>>> medium = Medium(permittivity=2, nonlinear_spec=nonlinear_spec)", + "type": "object", + "properties": { + "models": { + "title": "Nonlinear models", + "description": "The nonlinear models present in this nonlinear spec. Nonlinear models of different types are additive. Multiple nonlinear models of the same type are not allowed.", + "default": [], + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSusceptibility" + }, + { + "$ref": "#/definitions/TwoPhotonAbsorption" + }, + { + "$ref": "#/definitions/KerrNonlinearity" + } + ] + } + }, + "num_iters": { + "title": "Number of iterations", + "description": "Number of iterations for solving nonlinear constitutive relation.", + "default": 5, + "exclusiveMinimum": 0, + "type": "integer" + }, + "type": { + "title": "Type", + "default": "NonlinearSpec", + "enum": [ + "NonlinearSpec" + ], + "type": "string" + } + }, + "additionalProperties": false + }, + "SpaceModulation": { + "title": "SpaceModulation", + "description": "The modulation profile with a user-supplied spatial distribution of\namplitude and phase.\n\nParameters\n----------\namplitude : Union[float, SpatialDataArray] = 1\n Amplitude of modulation that can vary spatially. It takes the unit of whatever is being modulated.\nphase : Union[float, SpatialDataArray] = 0\n [units = rad]. Phase of modulation that can vary spatially.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Method of interpolation to use to obtain values at spatial locations on the Yee grids.\n\nNote\n----\n.. math::\n\n amp\\_space(r) = amplitude(r) \\cdot e^{i \\cdot phase(r)}\n\nThe full space-time modulation is,\n\n.. math::\n\n amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)]\n\nExample\n-------\n>>> Nx, Ny, Nz = 10, 9, 8\n>>> X = np.linspace(-1, 1, Nx)\n>>> Y = np.linspace(-1, 1, Ny)\n>>> Z = np.linspace(-1, 1, Nz)\n>>> coords = dict(x=X, y=Y, z=Z)\n>>> amp = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> phase = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> space = SpaceModulation(amplitude=amp, phase=phase)", + "type": "object", + "properties": { + "type": { + "title": "Type", + "default": "SpaceModulation", + "enum": [ + "SpaceModulation" + ], + "type": "string" + }, + "amplitude": { + "title": "Amplitude of modulation in space", + "description": "Amplitude of modulation that can vary spatially. It takes the unit of whatever is being modulated.", + "default": 1, + "anyOf": [ + { + "type": "number" + }, + { + "title": "DataArray", + "type": "xr.DataArray", + "properties": { + "_dims": { + "title": "_dims", + "type": "Tuple[str, ...]" + } + }, + "required": [ + "_dims" + ] + } + ] + }, + "phase": { + "title": "Phase of modulation in space", + "description": "Phase of modulation that can vary spatially.", + "default": 0, + "units": "rad", + "anyOf": [ + { + "type": "number" + }, + { + "title": "DataArray", + "type": "xr.DataArray", + "properties": { + "_dims": { + "title": "_dims", + "type": "Tuple[str, ...]" + } + }, + "required": [ + "_dims" + ] + } + ] + }, + "interp_method": { + "title": "Interpolation method", + "description": "Method of interpolation to use to obtain values at spatial locations on the Yee grids.", + "default": "nearest", + "enum": [ + "nearest", + "linear" + ], + "type": "string" + } + }, + "additionalProperties": false + }, + "ContinuousWaveTimeModulation": { + "title": "ContinuousWaveTimeModulation", + "description": "Class describing modulation with a harmonic time dependence.\n\nParameters\n----------\namplitude : NonNegativeFloat = 1.0\n Real-valued maximum amplitude of the time dependence.\nphase : float = 0.0\n [units = rad]. Phase shift of the time dependence.\nfreq0 : PositiveFloat\n [units = Hz]. Modulation frequency.\n\nNote\n----\n.. math::\n\n amp\\_time(t) = amplitude \\cdot \\\n e^{i \\cdot phase - 2 \\pi i \\cdot freq0 \\cdot t}\n\nNote\n----\nThe full space-time modulation is,\n\n.. math::\n\n amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)]\n\n\nExample\n-------\n>>> cw = ContinuousWaveTimeModulation(freq0=200e12, amplitude=1, phase=0)", + "type": "object", + "properties": { + "amplitude": { + "title": "Amplitude", + "description": "Real-valued maximum amplitude of the time dependence.", + "default": 1.0, + "minimum": 0, + "type": "number" + }, + "phase": { + "title": "Phase", + "description": "Phase shift of the time dependence.", + "default": 0.0, + "units": "rad", + "type": "number" + }, + "type": { + "title": "Type", + "default": "ContinuousWaveTimeModulation", + "enum": [ + "ContinuousWaveTimeModulation" + ], + "type": "string" + }, + "freq0": { + "title": "Modulation Frequency", + "description": "Modulation frequency.", + "units": "Hz", + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "freq0" + ], + "additionalProperties": false + }, + "SpaceTimeModulation": { + "title": "SpaceTimeModulation", + "description": "Space-time modulation applied to a medium, adding\non top of the time-independent part.\n\nParameters\n----------\nspace_modulation : SpaceModulation = SpaceModulation(type='SpaceModulation', amplitude=1.0, phase=0.0, interp_method='nearest')\n Space modulation part from the separable SpaceTimeModulation.\ntime_modulation : ContinuousWaveTimeModulation\n Time modulation part from the separable SpaceTimeModulation.\n\n\nNote\n----\nThe space-time modulation must be separable in space and time.\ne.g. when applied to permittivity,\n\n.. math::\n\n \\delta \\epsilon(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)]", + "type": "object", + "properties": { + "space_modulation": { + "title": "Space modulation", + "description": "Space modulation part from the separable SpaceTimeModulation.", + "default": { + "type": "SpaceModulation", + "amplitude": 1.0, + "phase": 0.0, + "interp_method": "nearest" + }, + "allOf": [ + { + "$ref": "#/definitions/SpaceModulation" + } + ] + }, + "time_modulation": { + "title": "Time modulation", + "description": "Time modulation part from the separable SpaceTimeModulation.", + "allOf": [ + { + "$ref": "#/definitions/ContinuousWaveTimeModulation" + } + ] + }, + "type": { + "title": "Type", + "default": "SpaceTimeModulation", + "enum": [ + "SpaceTimeModulation" + ], + "type": "string" + } + }, + "required": [ + "time_modulation" + ], + "additionalProperties": false + }, + "ModulationSpec": { + "title": "ModulationSpec", + "description": "Specification adding space-time modulation to the non-dispersive part of medium\nincluding relative permittivity at infinite frequency and electric conductivity.\n\n\nParameters\n----------\npermittivity : Optional[SpaceTimeModulation] = None\n Space-time modulation of relative permittivity at infinite frequency applied on top of the base permittivity at infinite frequency.\nconductivity : Optional[SpaceTimeModulation] = None\n Space-time modulation of electric conductivity applied on top of the base conductivity.", + "type": "object", + "properties": { + "permittivity": { + "title": "Space-time modulation of relative permittivity", + "description": "Space-time modulation of relative permittivity at infinite frequency applied on top of the base permittivity at infinite frequency.", + "allOf": [ + { + "$ref": "#/definitions/SpaceTimeModulation" + } + ] + }, + "conductivity": { + "title": "Space-time modulation of conductivity", + "description": "Space-time modulation of electric conductivity applied on top of the base conductivity.", + "allOf": [ + { + "$ref": "#/definitions/SpaceTimeModulation" + } + ] + }, + "type": { + "title": "Type", + "default": "ModulationSpec", + "enum": [ + "ModulationSpec" + ], + "type": "string" + } + }, + "additionalProperties": false + }, + "FluidSpec": { + "title": "FluidSpec", + "description": "Fluid medium.\n\nParameters\n----------\n\nExample\n-------\n>>> solid = FluidSpec()", + "type": "object", + "properties": { + "type": { + "title": "Type", + "default": "FluidSpec", + "enum": [ + "FluidSpec" + ], + "type": "string" + } + }, + "additionalProperties": false + }, + "SolidSpec": { + "title": "SolidSpec", + "description": "Solid medium.\n\nParameters\n----------\ncapacity : PositiveFloat\n [units = J/(kg*K)]. Volumetric heat capacity in unit of J/(kg*K).\nconductivity : PositiveFloat\n [units = W/(um*K)]. Thermal conductivity of material in units of W/(um*K).\n\nExample\n-------\n>>> solid = SolidSpec(\n... capacity=2,\n... conductivity=3,\n... )", + "type": "object", + "properties": { + "type": { + "title": "Type", + "default": "SolidSpec", + "enum": [ + "SolidSpec" + ], + "type": "string" + }, + "capacity": { + "title": "Heat capacity", + "description": "Volumetric heat capacity in unit of J/(kg*K).", + "units": "J/(kg*K)", + "exclusiveMinimum": 0, + "type": "number" + }, + "conductivity": { + "title": "Thermal conductivity", + "description": "Thermal conductivity of material in units of W/(um*K).", + "units": "W/(um*K)", + "exclusiveMinimum": 0, + "type": "number" + } + }, + "required": [ + "capacity", + "conductivity" ], "additionalProperties": false }, "Medium": { "title": "Medium", - "description": "Dispersionless medium.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\npermittivity : ConstrainedFloatValue = 1.0\n [units = None (relative permittivity)]. Relative permittivity.\nconductivity : float = 0.0\n [units = S/um]. Electric conductivity. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\n\nExample\n-------\n>>> dielectric = Medium(permittivity=4.0, name='my_medium')\n>>> eps = dielectric.eps_model(200e12)", + "description": "Dispersionless medium.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\npermittivity : ConstrainedFloatValue = 1.0\n [units = None (relative permittivity)]. Relative permittivity.\nconductivity : float = 0.0\n [units = S/um]. Electric conductivity. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\n\nExample\n-------\n>>> dielectric = Medium(permittivity=4.0, name='my_medium')\n>>> eps = dielectric.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -581,19 +1029,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "Medium", @@ -620,28 +1099,9 @@ }, "additionalProperties": false }, - "ComplexNumber": { - "title": "ComplexNumber", - "description": "Complex number with a well defined schema.", - "type": "object", - "properties": { - "real": { - "title": "Real", - "type": "number" - }, - "imag": { - "title": "Imag", - "type": "number" - } - }, - "required": [ - "real", - "imag" - ] - }, "PoleResidue": { "title": "PoleResidue", - "description": "A dispersive medium described by the pole-residue pair model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles : Tuple[Tuple[Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber], Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber]], ...] = ()\n [units = (rad/sec, rad/sec)]. Tuple of complex-valued (:math:`a_i, c_i`) poles for the model.\n\nNote\n----\n.. math::\n\n \\epsilon(\\omega) = \\epsilon_\\infty - \\sum_i\n \\left[\\frac{c_i}{j \\omega + a_i} +\n \\frac{c_i^*}{j \\omega + a_i^*}\\right]\n\nExample\n-------\n>>> pole_res = PoleResidue(eps_inf=2.0, poles=[((-1+2j), (3+4j)), ((-5+6j), (7+8j))])\n>>> eps = pole_res.eps_model(200e12)", + "description": "A dispersive medium described by the pole-residue pair model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles : Tuple[Tuple[Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber], Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber]], ...] = ()\n [units = (rad/sec, rad/sec)]. Tuple of complex-valued (:math:`a_i, c_i`) poles for the model.\n\nNote\n----\n.. math::\n\n \\epsilon(\\omega) = \\epsilon_\\infty - \\sum_i\n \\left[\\frac{c_i}{j \\omega + a_i} +\n \\frac{c_i^*}{j \\omega + a_i^*}\\right]\n\nExample\n-------\n>>> pole_res = PoleResidue(eps_inf=2.0, poles=[((-1+2j), (3+4j)), ((-5+6j), (7+8j))])\n>>> eps = pole_res.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -670,19 +1130,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "PoleResidue", @@ -773,7 +1264,7 @@ }, "Sellmeier": { "title": "Sellmeier", - "description": "A dispersive medium described by the Sellmeier model.\nThe frequency-dependence of the refractive index is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\ncoeffs : Tuple[Tuple[float, pydantic.v1.types.PositiveFloat], ...]\n [units = (None, um^2)]. List of Sellmeier (:math:`B_i, C_i`) coefficients.\n\nNote\n----\n.. math::\n\n n(\\lambda)^2 = 1 + \\sum_i \\frac{B_i \\lambda^2}{\\lambda^2 - C_i}\n\nExample\n-------\n>>> sellmeier_medium = Sellmeier(coeffs=[(1,2), (3,4)])\n>>> eps = sellmeier_medium.eps_model(200e12)", + "description": "A dispersive medium described by the Sellmeier model.\nThe frequency-dependence of the refractive index is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\ncoeffs : Tuple[Tuple[float, pydantic.v1.types.PositiveFloat], ...]\n [units = (None, um^2)]. List of Sellmeier (:math:`B_i, C_i`) coefficients.\n\nNote\n----\n.. math::\n\n n(\\lambda)^2 = 1 + \\sum_i \\frac{B_i \\lambda^2}{\\lambda^2 - C_i}\n\nExample\n-------\n>>> sellmeier_medium = Sellmeier(coeffs=[(1,2), (3,4)])\n>>> eps = sellmeier_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -796,22 +1287,53 @@ "type": "number" }, { - "type": "number" + "type": "number" + } + ] + }, + "allow_gain": { + "title": "Allow gain medium", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "default": false, + "type": "boolean" + }, + "nonlinear_spec": { + "title": "Nonlinear Spec", + "description": "Nonlinear spec applied on top of the base medium properties.", + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, + { + "$ref": "#/definitions/NonlinearSusceptibility" + } + ] + }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" } ] }, - "allow_gain": { - "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", - "default": false, - "type": "boolean" - }, - "nonlinear_spec": { - "title": "Nonlinear Spec", - "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ { - "$ref": "#/definitions/NonlinearSusceptibility" + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" } ] }, @@ -854,7 +1376,7 @@ }, "Lorentz": { "title": "Lorentz", - "description": "A dispersive medium described by the Lorentz model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[float, float, pydantic.v1.types.NonNegativeFloat], ...]\n [units = (None (relative permittivity), Hz, Hz)]. List of (:math:`\\Delta\\epsilon_i, f_i, \\delta_i`) values for model.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i f_i^2}{f_i^2 - 2jf\\delta_i - f^2}\n\nExample\n-------\n>>> lorentz_medium = Lorentz(eps_inf=2.0, coeffs=[(1,2,3), (4,5,6)])\n>>> eps = lorentz_medium.eps_model(200e12)", + "description": "A dispersive medium described by the Lorentz model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[float, float, pydantic.v1.types.NonNegativeFloat], ...]\n [units = (None (relative permittivity), Hz, Hz)]. List of (:math:`\\Delta\\epsilon_i, f_i, \\delta_i`) values for model.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i f_i^2}{f_i^2 - 2jf\\delta_i - f^2}\n\nExample\n-------\n>>> lorentz_medium = Lorentz(eps_inf=2.0, coeffs=[(1,2,3), (4,5,6)])\n>>> eps = lorentz_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -883,19 +1405,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "Lorentz", @@ -947,7 +1500,7 @@ }, "Debye": { "title": "Debye", - "description": "A dispersive medium described by the Debye model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[float, pydantic.v1.types.PositiveFloat], ...]\n [units = (None (relative permittivity), sec)]. List of (:math:`\\Delta\\epsilon_i, \\tau_i`) values for model.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i}{1 - jf\\tau_i}\n\nExample\n-------\n>>> debye_medium = Debye(eps_inf=2.0, coeffs=[(1,2),(3,4)])\n>>> eps = debye_medium.eps_model(200e12)", + "description": "A dispersive medium described by the Debye model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[float, pydantic.v1.types.PositiveFloat], ...]\n [units = (None (relative permittivity), sec)]. List of (:math:`\\Delta\\epsilon_i, \\tau_i`) values for model.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i}{1 - jf\\tau_i}\n\nExample\n-------\n>>> debye_medium = Debye(eps_inf=2.0, coeffs=[(1,2),(3,4)])\n>>> eps = debye_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -976,19 +1529,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "Debye", @@ -1036,7 +1620,7 @@ }, "Drude": { "title": "Drude", - "description": "A dispersive medium described by the Drude model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[float, pydantic.v1.types.PositiveFloat], ...]\n [units = (Hz, Hz)]. List of (:math:`f_i, \\delta_i`) values for model.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty - \\sum_i\n \\frac{ f_i^2}{f^2 + jf\\delta_i}\n\nExample\n-------\n>>> drude_medium = Drude(eps_inf=2.0, coeffs=[(1,2), (3,4)])\n>>> eps = drude_medium.eps_model(200e12)", + "description": "A dispersive medium described by the Drude model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[float, pydantic.v1.types.PositiveFloat], ...]\n [units = (Hz, Hz)]. List of (:math:`f_i, \\delta_i`) values for model.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty - \\sum_i\n \\frac{ f_i^2}{f^2 + jf\\delta_i}\n\nExample\n-------\n>>> drude_medium = Drude(eps_inf=2.0, coeffs=[(1,2), (3,4)])\n>>> eps = drude_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -1065,19 +1649,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "Drude", @@ -1123,9 +1738,95 @@ ], "additionalProperties": false }, + "PECMedium": { + "title": "PECMedium", + "description": "Perfect electrical conductor class.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\n\nNote\n----\nTo avoid confusion from duplicate PECs, must import ``tidy3d.PEC`` instance directly.", + "type": "object", + "properties": { + "name": { + "title": "Name", + "description": "Optional unique name for medium.", + "type": "string" + }, + "frequency_range": { + "title": "Frequency Range", + "description": "Optional range of validity for the medium.", + "units": [ + "Hz", + "Hz" + ], + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "number" + }, + { + "type": "number" + } + ] + }, + "allow_gain": { + "title": "Allow gain medium", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "default": false, + "type": "boolean" + }, + "nonlinear_spec": { + "title": "Nonlinear Spec", + "description": "Nonlinear spec applied on top of the base medium properties.", + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, + { + "$ref": "#/definitions/NonlinearSusceptibility" + } + ] + }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, + "type": { + "title": "Type", + "default": "PECMedium", + "enum": [ + "PECMedium" + ], + "type": "string" + } + }, + "additionalProperties": false + }, "AnisotropicMedium": { "title": "AnisotropicMedium", - "description": "Diagonally anisotropic medium.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : Optional[bool] = None\n This field is ignored. Please set ``allow_gain`` in each component\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nxx : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude]\n Medium describing the xx-component of the diagonal permittivity tensor.\nyy : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude]\n Medium describing the yy-component of the diagonal permittivity tensor.\nzz : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude]\n Medium describing the zz-component of the diagonal permittivity tensor.\n\nNote\n----\nOnly diagonal anisotropy is currently supported.\n\nExample\n-------\n>>> medium_xx = Medium(permittivity=4.0)\n>>> medium_yy = Medium(permittivity=4.1)\n>>> medium_zz = Medium(permittivity=3.9)\n>>> anisotropic_dielectric = AnisotropicMedium(xx=medium_xx, yy=medium_yy, zz=medium_zz)", + "description": "Diagonally anisotropic medium.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : Optional[bool] = None\n This field is ignored. Please set ``allow_gain`` in each component\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\nxx : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium]\n Medium describing the xx-component of the diagonal permittivity tensor.\nyy : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium]\n Medium describing the yy-component of the diagonal permittivity tensor.\nzz : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium]\n Medium describing the zz-component of the diagonal permittivity tensor.\n\nNote\n----\nOnly diagonal anisotropy is currently supported.\n\nExample\n-------\n>>> medium_xx = Medium(permittivity=4.0)\n>>> medium_yy = Medium(permittivity=4.1)\n>>> medium_zz = Medium(permittivity=3.9)\n>>> anisotropic_dielectric = AnisotropicMedium(xx=medium_xx, yy=medium_yy, zz=medium_zz)", "type": "object", "properties": { "name": { @@ -1160,12 +1861,43 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "AnisotropicMedium", @@ -1185,7 +1917,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -1206,6 +1939,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] }, @@ -1220,7 +1956,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -1241,6 +1978,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] }, @@ -1255,7 +1995,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -1276,6 +2017,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] } @@ -1287,9 +2031,9 @@ ], "additionalProperties": false }, - "PECMedium": { - "title": "PECMedium", - "description": "Perfect electrical conductor class.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\n\nNote\n----\nTo avoid confusion from duplicate PECs, must import ``tidy3d.PEC`` instance directly.", + "FullyAnisotropicMedium": { + "title": "FullyAnisotropicMedium", + "description": "Fully anisotropic medium including all 9 components of the permittivity and conductivity\ntensors. Provided permittivity tensor and the symmetric part of the conductivity tensor must\nhave coinciding main directions. A non-symmetric conductivity tensor can be used to model\nmagneto-optic effects. Note that dispersive properties and subpixel averaging are currently not\nsupported for fully anisotropic materials.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\npermittivity : ArrayLike[dtype=float, ndim=2, shape=(3, 3)] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]\n [units = None (relative permittivity)]. Relative permittivity tensor.\nconductivity : ArrayLike[dtype=float, ndim=2, shape=(3, 3)] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]\n [units = S/um]. Electric conductivity tensor. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\n\nNote\n----\nSimulations involving fully anisotropic materials are computationally more intensive, thus,\nthey take longer time to complete. This increase strongly depends on the filling fraction of\nthe simulation domain by fully anisotropic materials, varying approximately in the range from\n1.5 to 5. Cost of running a simulation is adjusted correspondingly.\n\nExample\n-------\n>>> perm = [[2, 0, 0], [0, 1, 0], [0, 0, 3]]\n>>> cond = [[0.1, 0, 0], [0, 0, 0], [0, 0, 0]]\n>>> anisotropic_dielectric = FullyAnisotropicMedium(permittivity=perm, conductivity=cond)", "type": "object", "properties": { "name": { @@ -1318,71 +2062,47 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, - "type": { - "title": "Type", - "default": "PECMedium", - "enum": [ - "PECMedium" - ], - "type": "string" - } - }, - "additionalProperties": false - }, - "FullyAnisotropicMedium": { - "title": "FullyAnisotropicMedium", - "description": "Fully anisotropic medium including all 9 components of the permittivity and conductivity\ntensors. Provided permittivity tensor and the symmetric part of the conductivity tensor must\nhave coinciding main directions. A non-symmetric conductivity tensor can be used to model\nmagneto-optic effects. Note that dispersive properties and subpixel averaging are currently not\nsupported for fully anisotropic materials.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\npermittivity : ArrayLike[dtype=float, ndim=2, shape=(3, 3)] = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]\n [units = None (relative permittivity)]. Relative permittivity tensor.\nconductivity : ArrayLike[dtype=float, ndim=2, shape=(3, 3)] = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]\n [units = S/um]. Electric conductivity tensor. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\n\nNote\n----\nSimulations involving fully anisotropic materials are computationally more intensive, thus,\nthey take longer time to complete. This increase strongly depends on the filling fraction of\nthe simulation domain by fully anisotropic materials, varying approximately in the range from\n1.5 to 5. Cost of running a simulation is adjusted correspondingly.\n\nExample\n-------\n>>> perm = [[2, 0, 0], [0, 1, 0], [0, 0, 3]]\n>>> cond = [[0.1, 0, 0], [0, 0, 0], [0, 0, 0]]\n>>> anisotropic_dielectric = FullyAnisotropicMedium(permittivity=perm, conductivity=cond)", - "type": "object", - "properties": { - "name": { - "title": "Name", - "description": "Optional unique name for medium.", - "type": "string" - }, - "frequency_range": { - "title": "Frequency Range", - "description": "Optional range of validity for the medium.", - "units": [ - "Hz", - "Hz" - ], - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": [ - { - "type": "number" - }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ { - "type": "number" + "$ref": "#/definitions/ModulationSpec" } ] }, - "allow_gain": { - "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", - "default": false, - "type": "boolean" - }, - "nonlinear_spec": { - "title": "Nonlinear Spec", - "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ { - "$ref": "#/definitions/NonlinearSusceptibility" + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" } ] }, @@ -1508,7 +2228,7 @@ }, "CustomMedium": { "title": "CustomMedium", - "description": ":class:`.Medium` with user-supplied permittivity distribution.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\npermittivity : Optional[SpatialDataArray] = None\n [units = None (relative permittivity)]. Spatial profile of relative permittivity.\nconductivity : Optional[SpatialDataArray] = None\n [units = S/um]. Spatial profile Electric conductivity. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\neps_dataset : Optional[PermittivityDataset] = None\n [To be deprecated] User-supplied dataset containing complex-valued permittivity as a function of space. Permittivity distribution over the Yee-grid will be interpolated based on ``interp_method``.\n\nExample\n-------\n>>> Nx, Ny, Nz = 10, 9, 8\n>>> X = np.linspace(-1, 1, Nx)\n>>> Y = np.linspace(-1, 1, Ny)\n>>> Z = np.linspace(-1, 1, Nz)\n>>> coords = dict(x=X, y=Y, z=Z)\n>>> permittivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> conductivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> dielectric = CustomMedium(permittivity=permittivity, conductivity=conductivity)\n>>> eps = dielectric.eps_model(200e12)", + "description": ":class:`.Medium` with user-supplied permittivity distribution.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\neps_dataset : Optional[PermittivityDataset] = None\n [To be deprecated] User-supplied dataset containing complex-valued permittivity as a function of space. Permittivity distribution over the Yee-grid will be interpolated based on ``interp_method``.\npermittivity : Optional[SpatialDataArray] = None\n [units = None (relative permittivity)]. Spatial profile of relative permittivity.\nconductivity : Optional[SpatialDataArray] = None\n [units = S/um]. Spatial profile Electric conductivity. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\n\nExample\n-------\n>>> Nx, Ny, Nz = 10, 9, 8\n>>> X = np.linspace(-1, 1, Nx)\n>>> Y = np.linspace(-1, 1, Ny)\n>>> Z = np.linspace(-1, 1, Nz)\n>>> coords = dict(x=X, y=Y, z=Z)\n>>> permittivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> conductivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> dielectric = CustomMedium(permittivity=permittivity, conductivity=conductivity)\n>>> eps = dielectric.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -1537,16 +2257,47 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, + { + "$ref": "#/definitions/NonlinearSusceptibility" + } + ] + }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", "allOf": [ { - "$ref": "#/definitions/NonlinearSusceptibility" + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" } ] }, @@ -1574,6 +2325,15 @@ "default": false, "type": "boolean" }, + "eps_dataset": { + "title": "Permittivity Dataset", + "description": "[To be deprecated] User-supplied dataset containing complex-valued permittivity as a function of space. Permittivity distribution over the Yee-grid will be interpolated based on ``interp_method``.", + "allOf": [ + { + "$ref": "#/definitions/PermittivityDataset" + } + ] + }, "permittivity": { "title": "DataArray", "description": "Spatial profile of relative permittivity.", @@ -1603,22 +2363,13 @@ "required": [ "_dims" ] - }, - "eps_dataset": { - "title": "Permittivity Dataset", - "description": "[To be deprecated] User-supplied dataset containing complex-valued permittivity as a function of space. Permittivity distribution over the Yee-grid will be interpolated based on ``interp_method``.", - "allOf": [ - { - "$ref": "#/definitions/PermittivityDataset" - } - ] } }, "additionalProperties": false }, "CustomPoleResidue": { "title": "CustomPoleResidue", - "description": "A spatially varying dispersive medium described by the pole-residue pair model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...] = ()\n [units = (rad/sec, rad/sec)]. Tuple of complex-valued (:math:`a_i, c_i`) poles for the model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(\\omega) = \\epsilon_\\infty - \\sum_i\n \\left[\\frac{c_i}{j \\omega + a_i} +\n \\frac{c_i^*}{j \\omega + a_i^*}\\right]\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(np.ones((5, 6, 7)), coords=coords)\n>>> a1 = SpatialDataArray(-np.random.random((5, 6, 7)), coords=coords)\n>>> c1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> a2 = SpatialDataArray(-np.random.random((5, 6, 7)), coords=coords)\n>>> c2 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> pole_res = CustomPoleResidue(eps_inf=eps_inf, poles=[(a1, c1), (a2, c2)])\n>>> eps = pole_res.eps_model(200e12)", + "description": "A spatially varying dispersive medium described by the pole-residue pair model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...] = ()\n [units = (rad/sec, rad/sec)]. Tuple of complex-valued (:math:`a_i, c_i`) poles for the model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(\\omega) = \\epsilon_\\infty - \\sum_i\n \\left[\\frac{c_i}{j \\omega + a_i} +\n \\frac{c_i^*}{j \\omega + a_i^*}\\right]\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(np.ones((5, 6, 7)), coords=coords)\n>>> a1 = SpatialDataArray(-np.random.random((5, 6, 7)), coords=coords)\n>>> c1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> a2 = SpatialDataArray(-np.random.random((5, 6, 7)), coords=coords)\n>>> c2 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> pole_res = CustomPoleResidue(eps_inf=eps_inf, poles=[(a1, c1), (a2, c2)])\n>>> eps = pole_res.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -1647,19 +2398,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "CustomPoleResidue", @@ -1750,7 +2532,7 @@ }, "CustomSellmeier": { "title": "CustomSellmeier", - "description": "A spatially varying dispersive medium described by the Sellmeier model.\nThe frequency-dependence of the refractive index is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (None, um^2)]. List of Sellmeier (:math:`B_i, C_i`) coefficients.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n n(\\lambda)^2 = 1 + \\sum_i \\frac{B_i \\lambda^2}{\\lambda^2 - C_i}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> b1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> c1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> sellmeier_medium = CustomSellmeier(coeffs=[(b1,c1),])\n>>> eps = sellmeier_medium.eps_model(200e12)", + "description": "A spatially varying dispersive medium described by the Sellmeier model.\nThe frequency-dependence of the refractive index is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (None, um^2)]. List of Sellmeier (:math:`B_i, C_i`) coefficients.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n n(\\lambda)^2 = 1 + \\sum_i \\frac{B_i \\lambda^2}{\\lambda^2 - C_i}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> b1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> c1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> sellmeier_medium = CustomSellmeier(coeffs=[(b1,c1),])\n>>> eps = sellmeier_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -1779,19 +2561,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "CustomSellmeier", @@ -1866,7 +2679,7 @@ }, "CustomLorentz": { "title": "CustomLorentz", - "description": "A spatially varying dispersive medium described by the Lorentz model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (None (relative permittivity), Hz, Hz)]. List of (:math:`\\Delta\\epsilon_i, f_i, \\delta_i`) values for model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i f_i^2}{f_i^2 - 2jf\\delta_i - f^2}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(np.ones((5, 6, 7)), coords=coords)\n>>> d_epsilon = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> f = SpatialDataArray(1+np.random.random((5, 6, 7)), coords=coords)\n>>> delta = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> lorentz_medium = CustomLorentz(eps_inf=eps_inf, coeffs=[(d_epsilon,f,delta),])\n>>> eps = lorentz_medium.eps_model(200e12)", + "description": "A spatially varying dispersive medium described by the Lorentz model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (None (relative permittivity), Hz, Hz)]. List of (:math:`\\Delta\\epsilon_i, f_i, \\delta_i`) values for model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i f_i^2}{f_i^2 - 2jf\\delta_i - f^2}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(np.ones((5, 6, 7)), coords=coords)\n>>> d_epsilon = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> f = SpatialDataArray(1+np.random.random((5, 6, 7)), coords=coords)\n>>> delta = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> lorentz_medium = CustomLorentz(eps_inf=eps_inf, coeffs=[(d_epsilon,f,delta),])\n>>> eps = lorentz_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -1895,19 +2708,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "CustomLorentz", @@ -2012,7 +2856,7 @@ }, "CustomDebye": { "title": "CustomDebye", - "description": "A spatially varying dispersive medium described by the Debye model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (None (relative permittivity), sec)]. List of (:math:`\\Delta\\epsilon_i, \\tau_i`) values for model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i}{1 - jf\\tau_i}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(1+np.random.random((5, 6, 7)), coords=coords)\n>>> eps1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> tau1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> debye_medium = CustomDebye(eps_inf=eps_inf, coeffs=[(eps1,tau1),])\n>>> eps = debye_medium.eps_model(200e12)", + "description": "A spatially varying dispersive medium described by the Debye model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (None (relative permittivity), sec)]. List of (:math:`\\Delta\\epsilon_i, \\tau_i`) values for model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty + \\sum_i\n \\frac{\\Delta\\epsilon_i}{1 - jf\\tau_i}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(1+np.random.random((5, 6, 7)), coords=coords)\n>>> eps1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> tau1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> debye_medium = CustomDebye(eps_inf=eps_inf, coeffs=[(eps1,tau1),])\n>>> eps = debye_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -2041,19 +2885,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "CustomDebye", @@ -2144,7 +3019,7 @@ }, "CustomDrude": { "title": "CustomDrude", - "description": "A spatially varying dispersive medium described by the Drude model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (Hz, Hz)]. List of (:math:`f_i, \\delta_i`) values for model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty - \\sum_i\n \\frac{ f_i^2}{f^2 + jf\\delta_i}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(np.ones((5, 6, 7)), coords=coords)\n>>> f1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> delta1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> drude_medium = CustomDrude(eps_inf=eps_inf, coeffs=[(f1,delta1),])\n>>> eps = drude_medium.eps_model(200e12)", + "description": "A spatially varying dispersive medium described by the Drude model.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : SpatialDataArray\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\ncoeffs : Tuple[Tuple[tidy3d.components.data.data_array.SpatialDataArray, tidy3d.components.data.data_array.SpatialDataArray], ...]\n [units = (Hz, Hz)]. List of (:math:`f_i, \\delta_i`) values for model.\ninterp_method : Literal['nearest', 'linear'] = nearest\n Interpolation method to obtain permittivity values that are not supplied at the Yee grids; For grids outside the range of the supplied data, extrapolation will be applied. When the extrapolated value is smaller (greater) than the minimal (maximal) of the supplied data, the extrapolated value will take the minimal (maximal) of the supplied data.\nsubpixel : bool = False\n If ``True`` and simulation's ``subpixel`` is also ``True``, applies subpixel averaging of the permittivity on the interface of the structure, including exterior boundary and intersection interfaces with other structures.\n\nNote\n----\n.. math::\n\n \\epsilon(f) = \\epsilon_\\infty - \\sum_i\n \\frac{ f_i^2}{f^2 + jf\\delta_i}\n\nExample\n-------\n>>> x = np.linspace(-1, 1, 5)\n>>> y = np.linspace(-1, 1, 6)\n>>> z = np.linspace(-1, 1, 7)\n>>> coords = dict(x=x, y=y, z=z)\n>>> eps_inf = SpatialDataArray(np.ones((5, 6, 7)), coords=coords)\n>>> f1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> delta1 = SpatialDataArray(np.random.random((5, 6, 7)), coords=coords)\n>>> drude_medium = CustomDrude(eps_inf=eps_inf, coeffs=[(f1,delta1),])\n>>> eps = drude_medium.eps_model(200e12)", "type": "object", "properties": { "name": { @@ -2173,19 +3048,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "CustomDrude", @@ -2276,7 +3182,7 @@ }, "CustomAnisotropicMedium": { "title": "CustomAnisotropicMedium", - "description": "Diagonally anisotropic medium with spatially varying permittivity in each component.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : Optional[bool] = None\n This field is ignored. Please set ``allow_gain`` in each component\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nxx : Union[CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomMedium]\n Medium describing the xx-component of the diagonal permittivity tensor.\nyy : Union[CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomMedium]\n Medium describing the yy-component of the diagonal permittivity tensor.\nzz : Union[CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomMedium]\n Medium describing the zz-component of the diagonal permittivity tensor.\ninterp_method : Optional[Literal['nearest', 'linear']] = None\n When the value is 'None', each component will follow its own interpolation method. When the value is other than 'None', the interpolation method specified by this field will override the one in each component.\nsubpixel : Optional[bool] = None\n This field is ignored. Please set ``subpixel`` in each component\n\nNote\n----\nOnly diagonal anisotropy is currently supported.\n\nExample\n-------\n>>> Nx, Ny, Nz = 10, 9, 8\n>>> x = np.linspace(-1, 1, Nx)\n>>> y = np.linspace(-1, 1, Ny)\n>>> z = np.linspace(-1, 1, Nz)\n>>> coords = dict(x=x, y=y, z=z)\n>>> permittivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> conductivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> medium_xx = CustomMedium(permittivity=permittivity, conductivity=conductivity)\n>>> medium_yy = CustomMedium(permittivity=permittivity, conductivity=conductivity)\n>>> d_epsilon = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> f = SpatialDataArray(1+np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> delta = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> medium_zz = CustomLorentz(eps_inf=permittivity, coeffs=[(d_epsilon,f,delta),])\n>>> anisotropic_dielectric = CustomAnisotropicMedium(xx=medium_xx, yy=medium_yy, zz=medium_zz)", + "description": "Diagonally anisotropic medium with spatially varying permittivity in each component.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : Optional[bool] = None\n This field is ignored. Please set ``allow_gain`` in each component\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\nxx : Union[CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomMedium]\n Medium describing the xx-component of the diagonal permittivity tensor.\nyy : Union[CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomMedium]\n Medium describing the yy-component of the diagonal permittivity tensor.\nzz : Union[CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomMedium]\n Medium describing the zz-component of the diagonal permittivity tensor.\ninterp_method : Optional[Literal['nearest', 'linear']] = None\n When the value is 'None', each component will follow its own interpolation method. When the value is other than 'None', the interpolation method specified by this field will override the one in each component.\nsubpixel : Optional[bool] = None\n This field is ignored. Please set ``subpixel`` in each component\n\nNote\n----\nOnly diagonal anisotropy is currently supported.\n\nExample\n-------\n>>> Nx, Ny, Nz = 10, 9, 8\n>>> x = np.linspace(-1, 1, Nx)\n>>> y = np.linspace(-1, 1, Ny)\n>>> z = np.linspace(-1, 1, Nz)\n>>> coords = dict(x=x, y=y, z=z)\n>>> permittivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> conductivity= SpatialDataArray(np.ones((Nx, Ny, Nz)), coords=coords)\n>>> medium_xx = CustomMedium(permittivity=permittivity, conductivity=conductivity)\n>>> medium_yy = CustomMedium(permittivity=permittivity, conductivity=conductivity)\n>>> d_epsilon = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> f = SpatialDataArray(1+np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> delta = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords)\n>>> medium_zz = CustomLorentz(eps_inf=permittivity, coeffs=[(d_epsilon,f,delta),])\n>>> anisotropic_dielectric = CustomAnisotropicMedium(xx=medium_xx, yy=medium_yy, zz=medium_zz)", "type": "object", "properties": { "name": { @@ -2311,12 +3217,43 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "CustomAnisotropicMedium", @@ -2817,7 +3754,7 @@ }, "PerturbationMedium": { "title": "PerturbationMedium", - "description": "Dispersionless medium with perturbations.\n\nParameters\n----------\nsubpixel : bool = True\n This value will be transferred to the resulting custom medium. That is, if ``True``, the subpixel averaging will be applied to the custom medium provided the corresponding ``Simulation``'s field ``subpixel`` is set to ``True`` as well. If the resulting medium is not a custom medium (no perturbations), this field does not have an effect.\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\npermittivity : ConstrainedFloatValue = 1.0\n [units = None (relative permittivity)]. Relative permittivity.\nconductivity : float = 0.0\n [units = S/um]. Electric conductivity. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\npermittivity_perturbation : Optional[ParameterPerturbation] = None\n [units = None (relative permittivity)]. List of heat and/or charge perturbations to permittivity.\nconductivity_perturbation : Optional[ParameterPerturbation] = None\n [units = S/um]. List of heat and/or charge perturbations to permittivity.\n\nExample\n-------\n>>> from tidy3d import ParameterPerturbation, LinearHeatPerturbation\n>>> dielectric = PerturbationMedium(\n... permittivity=4.0,\n... permittivity_perturbation=ParameterPerturbation(\n... heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001),\n... ),\n... name='my_medium',\n... )", + "description": "Dispersionless medium with perturbations.\n\nParameters\n----------\nsubpixel : bool = True\n This value will be transferred to the resulting custom medium. That is, if ``True``, the subpixel averaging will be applied to the custom medium provided the corresponding ``Simulation``'s field ``subpixel`` is set to ``True`` as well. If the resulting medium is not a custom medium (no perturbations), this field does not have an effect.\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\npermittivity : ConstrainedFloatValue = 1.0\n [units = None (relative permittivity)]. Relative permittivity.\nconductivity : float = 0.0\n [units = S/um]. Electric conductivity. Defined such that the imaginary part of the complex permittivity at angular frequency omega is given by conductivity/omega.\npermittivity_perturbation : Optional[ParameterPerturbation] = None\n [units = None (relative permittivity)]. List of heat and/or charge perturbations to permittivity.\nconductivity_perturbation : Optional[ParameterPerturbation] = None\n [units = S/um]. List of heat and/or charge perturbations to permittivity.\n\nExample\n-------\n>>> from tidy3d import ParameterPerturbation, LinearHeatPerturbation\n>>> dielectric = PerturbationMedium(\n... permittivity=4.0,\n... permittivity_perturbation=ParameterPerturbation(\n... heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001),\n... ),\n... name='my_medium',\n... )", "type": "object", "properties": { "subpixel": { @@ -2860,19 +3797,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "permittivity": { "title": "Permittivity", "description": "Relative permittivity.", @@ -2913,7 +3881,7 @@ }, "PerturbationPoleResidue": { "title": "PerturbationPoleResidue", - "description": "A dispersive medium described by the pole-residue pair model with perturbations.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nsubpixel : bool = True\n This value will be transferred to the resulting custom medium. That is, if ``True``, the subpixel averaging will be applied to the custom medium provided the corresponding ``Simulation``'s field ``subpixel`` is set to ``True`` as well. If the resulting medium is not a custom medium (no perturbations), this field does not have an effect.\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles : Tuple[Tuple[Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber], Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber]], ...] = ()\n [units = (rad/sec, rad/sec)]. Tuple of complex-valued (:math:`a_i, c_i`) poles for the model.\neps_inf_perturbation : Optional[ParameterPerturbation] = None\n [units = None (relative permittivity)]. Perturbations to relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles_perturbation : Tuple[Tuple[Optional[tidy3d.components.parameter_perturbation.ParameterPerturbation], Optional[tidy3d.components.parameter_perturbation.ParameterPerturbation]], ...] = ()\n [units = (rad/sec, rad/sec)]. Perturbations to poles of the model.\n\nNote\n----\n.. math::\n\n \\epsilon(\\omega) = \\epsilon_\\infty - \\sum_i\n \\left[\\frac{c_i}{j \\omega + a_i} +\n \\frac{c_i^*}{j \\omega + a_i^*}\\right]\n\nExample\n-------\n>>> from tidy3d import ParameterPerturbation, LinearHeatPerturbation\n>>> c0_perturbation = ParameterPerturbation(\n... heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001),\n... )\n>>> pole_res = PerturbationPoleResidue(\n... eps_inf=2.0,\n... poles=[((-1+2j), (3+4j)), ((-5+6j), (7+8j))],\n... poles_perturbation=[(None, c0_perturbation), (None, None)],\n... )", + "description": "A dispersive medium described by the pole-residue pair model with perturbations.\nThe frequency-dependence of the complex-valued permittivity is described by:\n\nParameters\n----------\nsubpixel : bool = True\n This value will be transferred to the resulting custom medium. That is, if ``True``, the subpixel averaging will be applied to the custom medium provided the corresponding ``Simulation``'s field ``subpixel`` is set to ``True`` as well. If the resulting medium is not a custom medium (no perturbations), this field does not have an effect.\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\neps_inf : PositiveFloat = 1.0\n [units = None (relative permittivity)]. Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles : Tuple[Tuple[Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber], Union[tidy3d.components.types.tidycomplex, tidy3d.components.types.ComplexNumber]], ...] = ()\n [units = (rad/sec, rad/sec)]. Tuple of complex-valued (:math:`a_i, c_i`) poles for the model.\neps_inf_perturbation : Optional[ParameterPerturbation] = None\n [units = None (relative permittivity)]. Perturbations to relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).\npoles_perturbation : Tuple[Tuple[Optional[tidy3d.components.parameter_perturbation.ParameterPerturbation], Optional[tidy3d.components.parameter_perturbation.ParameterPerturbation]], ...] = ()\n [units = (rad/sec, rad/sec)]. Perturbations to poles of the model.\n\nNote\n----\n.. math::\n\n \\epsilon(\\omega) = \\epsilon_\\infty - \\sum_i\n \\left[\\frac{c_i}{j \\omega + a_i} +\n \\frac{c_i^*}{j \\omega + a_i^*}\\right]\n\nExample\n-------\n>>> from tidy3d import ParameterPerturbation, LinearHeatPerturbation\n>>> c0_perturbation = ParameterPerturbation(\n... heat=LinearHeatPerturbation(temperature_ref=300, coeff=0.0001),\n... )\n>>> pole_res = PerturbationPoleResidue(\n... eps_inf=2.0,\n... poles=[((-1+2j), (3+4j)), ((-5+6j), (7+8j))],\n... poles_perturbation=[(None, c0_perturbation), (None, None)],\n... )", "type": "object", "properties": { "subpixel": { @@ -2956,19 +3924,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "eps_inf": { "title": "Epsilon at Infinity", "description": "Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).", @@ -3529,6 +4528,7 @@ "propertyName": "type", "mapping": { "Box": "#/definitions/Box", + "Transformed": "#/definitions/Transformed", "ClipOperation": "#/definitions/ClipOperation", "GeometryGroup": "#/definitions/GeometryGroup", "Sphere": "#/definitions/Sphere", @@ -3542,6 +4542,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -3603,6 +4606,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -3633,6 +4639,63 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, + { + "$ref": "#/definitions/ClipOperation" + }, + { + "$ref": "#/definitions/GeometryGroup" + }, + { + "$ref": "#/definitions/Sphere" + }, + { + "$ref": "#/definitions/Cylinder" + }, + { + "$ref": "#/definitions/PolySlab" + }, + { + "$ref": "#/definitions/ComplexPolySlabBase" + }, + { + "$ref": "#/definitions/TriangleMesh" + } + ] + } + }, + "required": [ + "operation", + "geometry_a", + "geometry_b" + ], + "additionalProperties": false + }, + "Transformed": { + "title": "Transformed", + "description": "Class representing a transformed geometry.\n\nParameters\n----------\ngeometry : ForwardRef('annotate_type(GeometryType)')\n Base geometry to be transformed.\ntransform : ArrayLike[dtype=float, ndim=2, shape=(4, 4)] = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0]]\n Transform matrix applied to the base geometry.", + "type": "object", + "properties": { + "type": { + "title": "Type", + "default": "Transformed", + "enum": [ + "Transformed" + ], + "type": "string" + }, + "geometry": { + "title": "Geometry", + "description": "Base geometry to be transformed.", + "anyOf": [ + { + "$ref": "#/definitions/Box" + }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -3655,18 +4718,47 @@ "$ref": "#/definitions/TriangleMesh" } ] + }, + "transform": { + "title": "Transform", + "description": "Transform matrix applied to the base geometry.", + "default": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 1.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 1.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0, + 1.0 + ] + ], + "type": "ArrayLike" } }, "required": [ - "operation", - "geometry_a", - "geometry_b" + "geometry" ], "additionalProperties": false }, "Medium2D": { "title": "Medium2D", - "description": "2D diagonally anisotropic medium.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Optional[NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nss : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude]\n Medium describing the ss-component of the diagonal permittivity tensor. The ss-component refers to the in-plane dimension of the medium that is the first component in order of 'x', 'y', 'z'. If the 2D material is normal to the y-axis, for example, then this determines the xx-component of the corresponding 3D medium.\ntt : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude]\n Medium describing the tt-component of the diagonal permittivity tensor. The tt-component refers to the in-plane dimension of the medium that is the second component in order of 'x', 'y', 'z'. If the 2D material is normal to the y-axis, for example, then this determines the zz-component of the corresponding 3D medium.\n\nNote\n----\nOnly diagonal anisotropy is currently supported.\n\nExample\n-------\n>>> drude_medium = Drude(eps_inf=2.0, coeffs=[(1,2), (3,4)])\n>>> medium2d = Medium2D(ss=drude_medium, tt=drude_medium)", + "description": "2D diagonally anisotropic medium.\n\nParameters\n----------\nname : Optional[str] = None\n Optional unique name for medium.\nfrequency_range : Optional[Tuple[float, float]] = None\n [units = (Hz, Hz)]. Optional range of validity for the medium.\nallow_gain : bool = False\n Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.\nnonlinear_spec : Union[NonlinearSpec, NonlinearSusceptibility] = None\n Nonlinear spec applied on top of the base medium properties.\nmodulation_spec : Optional[ModulationSpec] = None\n Modulation spec applied on top of the base medium properties.\nheat_spec : Union[FluidSpec, SolidSpec, NoneType] = None\n Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.\nss : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium]\n Medium describing the ss-component of the diagonal permittivity tensor. The ss-component refers to the in-plane dimension of the medium that is the first component in order of 'x', 'y', 'z'. If the 2D material is normal to the y-axis, for example, then this determines the xx-component of the corresponding 3D medium.\ntt : Union[Medium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, PECMedium]\n Medium describing the tt-component of the diagonal permittivity tensor. The tt-component refers to the in-plane dimension of the medium that is the second component in order of 'x', 'y', 'z'. If the 2D material is normal to the y-axis, for example, then this determines the zz-component of the corresponding 3D medium.\n\nNote\n----\nOnly diagonal anisotropy is currently supported.\n\nExample\n-------\n>>> drude_medium = Drude(eps_inf=2.0, coeffs=[(1,2), (3,4)])\n>>> medium2d = Medium2D(ss=drude_medium, tt=drude_medium)", "type": "object", "properties": { "name": { @@ -3695,19 +4787,50 @@ }, "allow_gain": { "title": "Allow gain medium", - "description": "Allow the medium to be active. Caution: simulations with gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", + "description": "Allow the medium to be active. Caution: simulations with a gain medium are unstable, and are likely to diverge.Simulations where 'allow_gain' is set to 'True' will still be charged even if diverged. Monitor data up to the divergence point will still be returned and can be useful in some cases.", "default": false, "type": "boolean" }, "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } ] }, + "modulation_spec": { + "title": "Modulation Spec", + "description": "Modulation spec applied on top of the base medium properties.", + "allOf": [ + { + "$ref": "#/definitions/ModulationSpec" + } + ] + }, + "heat_spec": { + "title": "Heat Specification", + "description": "Specification of the medium heat properties. They are used for solving the heat equation via the ``HeatSimulation`` interface. Such simulations can be used for investigating the influence of heat propagation on the properties of optical systems. Once the temperature distribution in the system is found using ``HeatSimulation`` object, ``Simulation.perturbed_mediums_copy()`` can be used to convert mediums with perturbation models defined into spatially dependent custom mediums. Otherwise, the ``heat_spec`` does not directly affect the running of an optical ``Simulation``.", + "discriminator": { + "propertyName": "type", + "mapping": { + "FluidSpec": "#/definitions/FluidSpec", + "SolidSpec": "#/definitions/SolidSpec" + } + }, + "oneOf": [ + { + "$ref": "#/definitions/FluidSpec" + }, + { + "$ref": "#/definitions/SolidSpec" + } + ] + }, "type": { "title": "Type", "default": "Medium2D", @@ -3727,7 +4850,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -3748,6 +4872,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] }, @@ -3762,7 +4889,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -3783,6 +4911,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] } @@ -3795,7 +4926,7 @@ }, "Structure": { "title": "Structure", - "description": "Defines a physical object that interacts with the electromagnetic fields.\nA :class:`Structure` is a combination of a material property (:class:`AbstractMedium`)\nand a :class:`Geometry`.\n\nParameters\n----------\ngeometry : Union[Box, ClipOperation, GeometryGroup, Sphere, Cylinder, PolySlab, ComplexPolySlabBase, TriangleMesh]\n Defines geometric properties of the structure.\nname : Optional[str] = None\n Optional name for the structure.\nmedium : Union[Medium, AnisotropicMedium, PECMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, FullyAnisotropicMedium, CustomMedium, CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomAnisotropicMedium, PerturbationMedium, PerturbationPoleResidue, Medium2D]\n Defines the electromagnetic properties of the structure's medium.\n\nExample\n-------\n>>> from tidy3d import Box, Medium\n>>> box = Box(center=(0,0,1), size=(2, 2, 2))\n>>> glass = Medium(permittivity=3.9)\n>>> struct = Structure(geometry=box, medium=glass, name='glass_box')", + "description": "Defines a physical object that interacts with the electromagnetic fields.\nA :class:`Structure` is a combination of a material property (:class:`AbstractMedium`)\nand a :class:`Geometry`.\n\nParameters\n----------\ngeometry : Union[Box, Transformed, ClipOperation, GeometryGroup, Sphere, Cylinder, PolySlab, ComplexPolySlabBase, TriangleMesh]\n Defines geometric properties of the structure.\nname : Optional[str] = None\n Optional name for the structure.\nmedium : Union[Medium, AnisotropicMedium, PECMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, FullyAnisotropicMedium, CustomMedium, CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomAnisotropicMedium, PerturbationMedium, PerturbationPoleResidue, Medium2D]\n Defines the electromagnetic properties of the structure's medium.\n\nExample\n-------\n>>> from tidy3d import Box, Medium\n>>> box = Box(center=(0,0,1), size=(2, 2, 2))\n>>> glass = Medium(permittivity=3.9)\n>>> struct = Structure(geometry=box, medium=glass, name='glass_box')", "type": "object", "properties": { "geometry": { @@ -3805,6 +4936,7 @@ "propertyName": "type", "mapping": { "Box": "#/definitions/Box", + "Transformed": "#/definitions/Transformed", "ClipOperation": "#/definitions/ClipOperation", "GeometryGroup": "#/definitions/GeometryGroup", "Sphere": "#/definitions/Sphere", @@ -3818,6 +4950,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -4011,7 +5146,7 @@ }, "ContinuousWave": { "title": "ContinuousWave", - "description": "Source time dependence that ramps up to continuous oscillation\nand holds until end of simulation.\n\nParameters\n----------\namplitude : NonNegativeFloat = 1.0\n Real-valued maximum amplitude of the time dependence.\nphase : float = 0.0\n [units = rad]. Phase shift of the time dependence.\nfreq0 : PositiveFloat\n [units = Hz]. Central frequency of the pulse.\nfwidth : PositiveFloat\n [units = Hz]. Standard deviation of the frequency content of the pulse.\noffset : ConstrainedFloatValue = 5.0\n Time delay of the maximum value of the pulse in units of 1 / (``2pi * fwidth``).\n\nExample\n-------\n>>> cw = ContinuousWave(freq0=200e12, fwidth=20e12)", + "description": "Source time dependence that ramps up to continuous oscillation\nand holds until end of simulation.\n\nParameters\n----------\namplitude : NonNegativeFloat = 1.0\n Real-valued maximum amplitude of the time dependence.\nphase : float = 0.0\n [units = rad]. Phase shift of the time dependence.\nfreq0 : PositiveFloat\n [units = Hz]. Central frequency of the pulse.\nfwidth : PositiveFloat\n [units = Hz]. Standard deviation of the frequency content of the pulse.\noffset : ConstrainedFloatValue = 5.0\n Time delay of the maximum value of the pulse in units of 1 / (``2pi * fwidth``).\n\nNote\n----\nField decay will not occur, so the simulation will run for the full ``run_time``.\nAlso, source normalization of frequency-domain monitors is not meaningful.\n\nExample\n-------\n>>> cw = ContinuousWave(freq0=200e12, fwidth=20e12)", "type": "object", "properties": { "amplitude": { @@ -4099,7 +5234,7 @@ }, "CustomSourceTime": { "title": "CustomSourceTime", - "description": "Custom source time dependence consisting of a real or complex envelope\nmodulated at a central frequency, as shown below.\n\nParameters\n----------\namplitude : NonNegativeFloat = 1.0\n Real-valued maximum amplitude of the time dependence.\nphase : float = 0.0\n [units = rad]. Phase shift of the time dependence.\nfreq0 : PositiveFloat\n [units = Hz]. Central frequency of the pulse.\nfwidth : PositiveFloat\n [units = Hz]. Standard deviation of the frequency content of the pulse.\noffset : float = 0.0\n Time delay of the envelope in units of 1 / (``2pi * fwidth``).\nsource_time_dataset : Optional[TimeDataset]\n Dataset for storing the envelope of the custom source time. This envelope will be modulated by a complex exponential at frequency ``freq0``.\n\nNote\n----\n.. math::\n\n amp\\_time(t) = amplitude \\cdot \\\n e^{i \\cdot phase - 2 \\pi i \\cdot freq0 \\cdot t} \\cdot \\\n envelope(t - offset / (2 \\pi \\cdot fwidth))\n\nNote\n----\nThe source time dependence is linearly interpolated to the simulation time steps.\nThe sampling rate should be sufficiently fast that this interpolation does not\nintroduce artifacts. The source time dependence should also start at zero and ramp up smoothly.\nThe first and last values of the envelope will be used for times that are out of range\nof the provided data.\n\nExample\n-------\n>>> cst = CustomSourceTime.from_values(freq0=1, fwidth=0.1,\n... values=np.linspace(0, 9, 10), dt=0.1)", + "description": "Custom source time dependence consisting of a real or complex envelope\nmodulated at a central frequency, as shown below.\n\nParameters\n----------\namplitude : NonNegativeFloat = 1.0\n Real-valued maximum amplitude of the time dependence.\nphase : float = 0.0\n [units = rad]. Phase shift of the time dependence.\nfreq0 : PositiveFloat\n [units = Hz]. Central frequency of the pulse.\nfwidth : PositiveFloat\n [units = Hz]. Standard deviation of the frequency content of the pulse.\noffset : float = 0.0\n Time delay of the envelope in units of 1 / (``2pi * fwidth``).\nsource_time_dataset : Optional[TimeDataset]\n Dataset for storing the envelope of the custom source time. This envelope will be modulated by a complex exponential at frequency ``freq0``.\n\nNote\n----\n.. math::\n\n amp\\_time(t) = amplitude \\cdot \\\n e^{i \\cdot phase - 2 \\pi i \\cdot freq0 \\cdot t} \\cdot \\\n envelope(t - offset / (2 \\pi \\cdot fwidth))\n\nNote\n----\nDepending on the envelope, field decay may not occur.\nIf field decay does not occur, then the simulation will run for the full ``run_time``.\nAlso, if field decay does not occur, then source normalization of frequency-domain\nmonitors is not meaningful.\n\nNote\n----\nThe source time dependence is linearly interpolated to the simulation time steps.\nThe sampling rate should be sufficiently fast that this interpolation does not\nintroduce artifacts. The source time dependence should also start at zero and ramp up smoothly.\nThe first and last values of the envelope will be used for times that are out of range\nof the provided data.\n\nExample\n-------\n>>> cst = CustomSourceTime.from_values(freq0=1, fwidth=0.1,\n... values=np.linspace(0, 9, 10), dt=0.1)", "type": "object", "properties": { "amplitude": { @@ -4163,9 +5298,14 @@ }, "UniformCurrentSource": { "title": "UniformCurrentSource", - "description": "Source in a rectangular volume with uniform time dependence. size=(0,0,0) gives point source.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\ninterpolate : bool = True\n Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.\npolarization : Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Specifies the direction and type of current component.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> pt_source = UniformCurrentSource(size=(0,0,0), source_time=pulse, polarization='Ex')", + "description": "Source in a rectangular volume with uniform time dependence. size=(0,0,0) gives point source.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\ninterpolate : bool = True\n Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.\npolarization : Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Specifies the direction and type of current component.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> pt_source = UniformCurrentSource(size=(0,0,0), source_time=pulse, polarization='Ex')", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "UniformCurrentSource", @@ -4243,11 +5383,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "interpolate": { "title": "Enable Interpolation", "description": "Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.", @@ -4277,9 +5412,14 @@ }, "PointDipole": { "title": "PointDipole", - "description": "Uniform current source with a zero size.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[typing_extensions.Literal[0], typing_extensions.Literal[0], typing_extensions.Literal[0]] = (0, 0, 0)\n [units = um]. Size in x, y, and z directions, constrained to ``(0, 0, 0)``.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\ninterpolate : bool = True\n Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.\npolarization : Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Specifies the direction and type of current component.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> pt_dipole = PointDipole(center=(1,2,3), source_time=pulse, polarization='Ex')", + "description": "Uniform current source with a zero size.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[typing_extensions.Literal[0], typing_extensions.Literal[0], typing_extensions.Literal[0]] = (0, 0, 0)\n [units = um]. Size in x, y, and z directions, constrained to ``(0, 0, 0)``.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\ninterpolate : bool = True\n Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.\npolarization : Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Specifies the direction and type of current component.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> pt_dipole = PointDipole(center=(1,2,3), source_time=pulse, polarization='Ex')", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "PointDipole", @@ -4368,11 +5508,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "interpolate": { "title": "Enable Interpolation", "description": "Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.", @@ -4401,9 +5536,14 @@ }, "GaussianBeam": { "title": "GaussianBeam", - "description": "Guassian distribution on finite extent plane.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\nnum_freqs : ConstrainedIntValue = 1\n Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\nwaist_radius : PositiveFloat = 1.0\n [units = um]. Radius of the beam at the waist.\nwaist_distance : float = 0.0\n [units = um]. Distance from the beam waist along the propagation direction. When ``direction`` is ``+`` and ``waist_distance`` is positive, the waist is on the ``-`` side (behind) the source plane. When ``direction`` is ``+`` and ``waist_distance``is negative, the waist is on the ``+`` side (in front) of the source plane.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> gauss = GaussianBeam(\n... size=(0,3,3),\n... source_time=pulse,\n... pol_angle=np.pi / 2,\n... direction='+',\n... waist_radius=1.0)", + "description": "Guassian distribution on finite extent plane.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nnum_freqs : ConstrainedIntValue = 1\n Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\nwaist_radius : PositiveFloat = 1.0\n [units = um]. Radius of the beam at the waist.\nwaist_distance : float = 0.0\n [units = um]. Distance from the beam waist along the propagation direction. When ``direction`` is ``+`` and ``waist_distance`` is positive, the waist is on the ``-`` side (behind) the source plane. When ``direction`` is ``+`` and ``waist_distance``is negative, the waist is on the ``+`` side (in front) of the source plane.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> gauss = GaussianBeam(\n... size=(0,3,3),\n... source_time=pulse,\n... pol_angle=np.pi / 2,\n... direction='+',\n... waist_radius=1.0)", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "GaussianBeam", @@ -4481,11 +5621,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "num_freqs": { "title": "Number of Frequency Points", "description": "Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.", @@ -4549,9 +5684,14 @@ }, "AstigmaticGaussianBeam": { "title": "AstigmaticGaussianBeam", - "description": "This class implements the simple astigmatic Gaussian beam described in Kochkina et al.,\nApplied Optics, vol. 52, issue 24, 2013. The simple astigmatic Guassian distribution allows\nboth an elliptical intensity profile and different waist locations for the two principal axes\nof the ellipse. When equal waist sizes and equal waist distances are specified in the two\ndirections, this source becomes equivalent to :class:`GaussianBeam`.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\nnum_freqs : ConstrainedIntValue = 1\n Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\nwaist_sizes : Tuple[PositiveFloat, PositiveFloat] = (1.0, 1.0)\n [units = um]. Size of the beam at the waist in the local x and y directions.\nwaist_distances : Tuple[float, float] = (0.0, 0.0)\n [units = um]. Distance to the beam waist along the propagation direction for the waist sizes in the local x and y directions. When ``direction`` is ``+`` and ``waist_distances`` are positive, the waist is on the ``-`` side (behind) the source plane. When ``direction`` is ``+`` and ``waist_distances`` are negative, the waist is on the ``+`` side (in front) of the source plane.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> gauss = AstigmaticGaussianBeam(\n... size=(0,3,3),\n... source_time=pulse,\n... pol_angle=np.pi / 2,\n... direction='+',\n... waist_sizes=(1.0, 2.0),\n... waist_distances = (3.0, 4.0))", + "description": "This class implements the simple astigmatic Gaussian beam described in Kochkina et al.,\nApplied Optics, vol. 52, issue 24, 2013. The simple astigmatic Guassian distribution allows\nboth an elliptical intensity profile and different waist locations for the two principal axes\nof the ellipse. When equal waist sizes and equal waist distances are specified in the two\ndirections, this source becomes equivalent to :class:`GaussianBeam`.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nnum_freqs : ConstrainedIntValue = 1\n Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\nwaist_sizes : Tuple[PositiveFloat, PositiveFloat] = (1.0, 1.0)\n [units = um]. Size of the beam at the waist in the local x and y directions.\nwaist_distances : Tuple[float, float] = (0.0, 0.0)\n [units = um]. Distance to the beam waist along the propagation direction for the waist sizes in the local x and y directions. When ``direction`` is ``+`` and ``waist_distances`` are positive, the waist is on the ``-`` side (behind) the source plane. When ``direction`` is ``+`` and ``waist_distances`` are negative, the waist is on the ``+`` side (in front) of the source plane.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> gauss = AstigmaticGaussianBeam(\n... size=(0,3,3),\n... source_time=pulse,\n... pol_angle=np.pi / 2,\n... direction='+',\n... waist_sizes=(1.0, 2.0),\n... waist_distances = (3.0, 4.0))", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "AstigmaticGaussianBeam", @@ -4629,11 +5769,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "num_freqs": { "title": "Number of Frequency Points", "description": "Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.", @@ -4847,9 +5982,14 @@ }, "ModeSource": { "title": "ModeSource", - "description": "Injects current source to excite modal profile on finite extent plane.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\nnum_freqs : ConstrainedIntValue = 1\n Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nmode_spec : ModeSpec = ModeSpec(num_modes=1, target_neff=None, num_pml=(0,, 0), filter_pol=None, angle_theta=0.0, angle_phi=0.0, precision='single', bend_radius=None, bend_axis=None, track_freq='central', group_index_step=False, type='ModeSpec')\n Parameters to feed to mode solver which determine modes measured by monitor.\nmode_index : NonNegativeInt = 0\n Index into the collection of modes returned by mode solver. Specifies which mode to inject using this source. If larger than ``mode_spec.num_modes``, ``num_modes`` in the solver will be set to ``mode_index + 1``.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> mode_spec = ModeSpec(target_neff=2.)\n>>> mode_source = ModeSource(\n... size=(10,10,0),\n... source_time=pulse,\n... mode_spec=mode_spec,\n... mode_index=1,\n... direction='-')", + "description": "Injects current source to excite modal profile on finite extent plane.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nnum_freqs : ConstrainedIntValue = 1\n Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nmode_spec : ModeSpec = ModeSpec(num_modes=1, target_neff=None, num_pml=(0,, 0), filter_pol=None, angle_theta=0.0, angle_phi=0.0, precision='single', bend_radius=None, bend_axis=None, track_freq='central', group_index_step=False, type='ModeSpec')\n Parameters to feed to mode solver which determine modes measured by monitor.\nmode_index : NonNegativeInt = 0\n Index into the collection of modes returned by mode solver. Specifies which mode to inject using this source. If larger than ``mode_spec.num_modes``, ``num_modes`` in the solver will be set to ``mode_index + 1``.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> mode_spec = ModeSpec(target_neff=2.)\n>>> mode_source = ModeSource(\n... size=(10,10,0),\n... source_time=pulse,\n... mode_spec=mode_spec,\n... mode_index=1,\n... direction='-')", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "ModeSource", @@ -4927,11 +6067,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "num_freqs": { "title": "Number of Frequency Points", "description": "Number of points used to approximate the frequency dependence of injected field. A Chebyshev interpolation is used, thus, only a small number of points, i.e., less than 20, is typically sufficient to obtain converged results.", @@ -4992,9 +6127,14 @@ }, "PlaneWave": { "title": "PlaneWave", - "description": "Uniform current distribution on an infinite extent plane. One element of size must be zero.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> pw_source = PlaneWave(size=(inf,0,inf), source_time=pulse, pol_angle=0.1, direction='+')", + "description": "Uniform current distribution on an infinite extent plane. One element of size must be zero.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\n\nExample\n-------\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> pw_source = PlaneWave(size=(inf,0,inf), source_time=pulse, pol_angle=0.1, direction='+')", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "PlaneWave", @@ -5072,11 +6212,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "direction": { "title": "Direction", "description": "Specifies propagation in the positive or negative direction of the injection axis.", @@ -5217,9 +6352,14 @@ }, "CustomFieldSource": { "title": "CustomFieldSource", - "description": "Implements a source corresponding to an input dataset containing ``E`` and ``H`` fields,\nusing the equivalence principle to define the actual injected currents. For the injection to\nwork as expected (i.e. to reproduce the required ``E`` and ``H`` fields), the field data must\ndecay by the edges of the source plane, or the source plane must span the entire simulation\ndomain and the fields must match the simulation boundary conditions.\nThe equivalent source currents are fully defined by the field components tangential to the\nsource plane. For e.g. source normal along ``z``, the normal components (``Ez`` and ``Hz``)\ncan be provided but will have no effect on the results, and at least one of the tangential\ncomponents has to be in the dataset, i.e. at least one of ``Ex``, ``Ey``, ``Hx``, and ``Hy``.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\nfield_dataset : Optional[FieldDataset]\n :class:`.FieldDataset` containing the desired frequency-domain fields patterns to inject. At least one tangetial field component must be specified.\n\nNote\n----\n The coordinates of all provided fields are assumed to be relative to the source center.\n\nNote\n----\n If only the ``E`` or only the ``H`` fields are provided, the source will not be directional,\n but will inject equal power in both directions instead.\n\nExample\n-------\n>>> from tidy3d import ScalarFieldDataArray\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> x = np.linspace(-1, 1, 101)\n>>> y = np.linspace(-1, 1, 101)\n>>> z = np.array([0])\n>>> f = [2e14]\n>>> coords = dict(x=x, y=y, z=z, f=f)\n>>> scalar_field = ScalarFieldDataArray(np.ones((101, 101, 1, 1)), coords=coords)\n>>> dataset = FieldDataset(Ex=scalar_field)\n>>> custom_source = CustomFieldSource(\n... center=(1, 1, 1),\n... size=(2, 2, 0),\n... source_time=pulse,\n... field_dataset=dataset)", + "description": "Implements a source corresponding to an input dataset containing ``E`` and ``H`` fields,\nusing the equivalence principle to define the actual injected currents. For the injection to\nwork as expected (i.e. to reproduce the required ``E`` and ``H`` fields), the field data must\ndecay by the edges of the source plane, or the source plane must span the entire simulation\ndomain and the fields must match the simulation boundary conditions.\nThe equivalent source currents are fully defined by the field components tangential to the\nsource plane. For e.g. source normal along ``z``, the normal components (``Ez`` and ``Hz``)\ncan be provided but will have no effect on the results, and at least one of the tangential\ncomponents has to be in the dataset, i.e. at least one of ``Ex``, ``Ey``, ``Hx``, and ``Hy``.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nfield_dataset : Optional[FieldDataset]\n :class:`.FieldDataset` containing the desired frequency-domain fields patterns to inject. At least one tangetial field component must be specified.\n\nNote\n----\n The coordinates of all provided fields are assumed to be relative to the source center.\n\nNote\n----\n If only the ``E`` or only the ``H`` fields are provided, the source will not be directional,\n but will inject equal power in both directions instead.\n\nExample\n-------\n>>> from tidy3d import ScalarFieldDataArray\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> x = np.linspace(-1, 1, 101)\n>>> y = np.linspace(-1, 1, 101)\n>>> z = np.array([0])\n>>> f = [2e14]\n>>> coords = dict(x=x, y=y, z=z, f=f)\n>>> scalar_field = ScalarFieldDataArray(np.ones((101, 101, 1, 1)), coords=coords)\n>>> dataset = FieldDataset(Ex=scalar_field)\n>>> custom_source = CustomFieldSource(\n... center=(1, 1, 1),\n... size=(2, 2, 0),\n... source_time=pulse,\n... field_dataset=dataset)", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "CustomFieldSource", @@ -5297,11 +6437,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "field_dataset": { "title": "Field Dataset", "description": ":class:`.FieldDataset` containing the desired frequency-domain fields patterns to inject. At least one tangetial field component must be specified.", @@ -5321,9 +6456,14 @@ }, "CustomCurrentSource": { "title": "CustomCurrentSource", - "description": "Implements a source corresponding to an input dataset containing ``E`` and ``H`` fields.\nInjects the specified components of the ``E`` and ``H`` dataset directly as ``J`` and ``M``\ncurrent distributions in the FDTD solver.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\ninterpolate : bool = True\n Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.\ncurrent_dataset : Optional[FieldDataset]\n :class:`.FieldDataset` containing the desired frequency-domain electric and magnetic current patterns to inject.\n\nNote\n----\n The coordinates of all provided fields are assumed to be relative to the source center.\n\nExample\n-------\n>>> from tidy3d import ScalarFieldDataArray\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> x = np.linspace(-1, 1, 101)\n>>> y = np.linspace(-1, 1, 101)\n>>> z = np.array([0])\n>>> f = [2e14]\n>>> coords = dict(x=x, y=y, z=z, f=f)\n>>> scalar_field = ScalarFieldDataArray(np.ones((101, 101, 1, 1)), coords=coords)\n>>> dataset = FieldDataset(Ex=scalar_field)\n>>> custom_source = CustomCurrentSource(\n... center=(1, 1, 1),\n... size=(2, 2, 0),\n... source_time=pulse,\n... current_dataset=dataset)", + "description": "Implements a source corresponding to an input dataset containing ``E`` and ``H`` fields.\nInjects the specified components of the ``E`` and ``H`` dataset directly as ``J`` and ``M``\ncurrent distributions in the FDTD solver.\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\ninterpolate : bool = True\n Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.\ncurrent_dataset : Optional[FieldDataset]\n :class:`.FieldDataset` containing the desired frequency-domain electric and magnetic current patterns to inject.\n\nNote\n----\n The coordinates of all provided fields are assumed to be relative to the source center.\n\nExample\n-------\n>>> from tidy3d import ScalarFieldDataArray\n>>> pulse = GaussianPulse(freq0=200e12, fwidth=20e12)\n>>> x = np.linspace(-1, 1, 101)\n>>> y = np.linspace(-1, 1, 101)\n>>> z = np.array([0])\n>>> f = [2e14]\n>>> coords = dict(x=x, y=y, z=z, f=f)\n>>> scalar_field = ScalarFieldDataArray(np.ones((101, 101, 1, 1)), coords=coords)\n>>> dataset = FieldDataset(Ex=scalar_field)\n>>> custom_source = CustomCurrentSource(\n... center=(1, 1, 1),\n... size=(2, 2, 0),\n... source_time=pulse,\n... current_dataset=dataset)", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "CustomCurrentSource", @@ -5401,11 +6541,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "interpolate": { "title": "Enable Interpolation", "description": "Handles reverse-interpolation of zero-size dimensions of the source. If ``False``, the source data is snapped to the nearest Yee grid point. If ``True``, equivalent source data is applied on the surrounding Yee grid points to emulate placement at the specified location using linear interpolation.", @@ -5431,9 +6566,14 @@ }, "TFSF": { "title": "TFSF", - "description": "Total-field scattered-field (TFSF) source that can inject a plane wave in a finite region.\nThe TFSF source injects 1 W / um^2 of power along the ``injection_axis``. Note that in the\ncase of angled incidence, 1 W / um^2 is still injected along the source's ``injection_axis``,\nand not the propagation direction, unlike a ``PlaneWave`` source. This allows computing\nscattering and absorption cross sections without the need for additional normalization.\n\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\nname : Optional[str] = None\n Optional name for the source.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\ninjection_axis : Literal[0, 1, 2]\n Specifies the injection axis. The plane of incidence is defined via this ``injection_axis`` and the ``direction``. The popagation axis is defined with respect to the ``injection_axis`` by ``angle_theta`` and ``angle_phi``.", + "description": "Total-field scattered-field (TFSF) source that can inject a plane wave in a finite region.\nThe TFSF source injects 1 W / um^2 of power along the ``injection_axis``. Note that in the\ncase of angled incidence, 1 W / um^2 is still injected along the source's ``injection_axis``,\nand not the propagation direction, unlike a ``PlaneWave`` source. This allows computing\nscattering and absorption cross sections without the need for additional normalization.\n\n\nParameters\n----------\nname : Optional[str] = None\n Optional name for the source.\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nsource_time : Union[GaussianPulse, ContinuousWave, CustomSourceTime]\n Specification of the source time-dependence.\ndirection : Literal['+', '-']\n Specifies propagation in the positive or negative direction of the injection axis.\nangle_theta : float = 0.0\n [units = rad]. Polar angle of the propagation axis from the injection axis.\nangle_phi : float = 0.0\n [units = rad]. Azimuth angle of the propagation axis in the plane orthogonal to the injection axis.\npol_angle : float = 0\n [units = rad]. Specifies the angle between the electric field polarization of the source and the plane defined by the injection axis and the propagation axis (rad). ``pol_angle=0`` (default) specifies P polarization, while ``pol_angle=np.pi/2`` specifies S polarization. At normal incidence when S and P are undefined, ``pol_angle=0`` defines: - ``Ey`` polarization for propagation along ``x``.- ``Ex`` polarization for propagation along ``y``.- ``Ex`` polarization for propagation along ``z``.\ninjection_axis : Literal[0, 1, 2]\n Specifies the injection axis. The plane of incidence is defined via this ``injection_axis`` and the ``direction``. The popagation axis is defined with respect to the ``injection_axis`` by ``angle_theta`` and ``angle_phi``.", "type": "object", "properties": { + "name": { + "title": "Name", + "description": "Optional name for the source.", + "type": "string" + }, "type": { "title": "Type", "default": "TFSF", @@ -5511,11 +6651,6 @@ } ] }, - "name": { - "title": "Name", - "description": "Optional name for the source.", - "type": "string" - }, "direction": { "title": "Direction", "description": "Specifies propagation in the positive or negative direction of the injection axis.", @@ -6239,7 +7374,7 @@ }, "FieldMonitor": { "title": "FieldMonitor", - "description": ":class:`Monitor` that records electromagnetic fields in the frequency domain.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included.\ncolocate : Optional[bool] = None\n Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nfields : Tuple[Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'], ...] = ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Collection of field components to store in the monitor.\n\nExample\n-------\n>>> monitor = FieldMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... fields=['Hx'],\n... freqs=[250e12, 300e12],\n... name='steady_state_monitor',\n... colocate=True)", + "description": ":class:`Monitor` that records electromagnetic fields in the frequency domain.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included.\ncolocate : bool = True\n Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nfields : Tuple[Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'], ...] = ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Collection of field components to store in the monitor.\n\nExample\n-------\n>>> monitor = FieldMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... fields=['Hx'],\n... freqs=[250e12, 300e12],\n... name='steady_state_monitor',\n... colocate=True)", "type": "object", "properties": { "type": { @@ -6303,8 +7438,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included.", "default": [ 1, 1, @@ -6329,8 +7464,9 @@ ] }, "colocate": { - "title": "Colocate fields", - "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.", + "title": "Colocate Fields", + "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", + "default": true, "type": "boolean" }, "freqs": { @@ -6398,7 +7534,7 @@ }, "FieldTimeMonitor": { "title": "FieldTimeMonitor", - "description": ":class:`Monitor` that records electromagnetic fields in the time domain.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included.\ncolocate : Optional[bool] = None\n Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.\nstart : NonNegativeFloat = 0.0\n [units = sec]. Time at which to start monitor recording.\nstop : Optional[NonNegativeFloat] = None\n [units = sec]. Time at which to stop monitor recording. If not specified, record until end of simulation.\ninterval : Optional[PositiveInt] = None\n Sampling rate of the monitor: number of time steps between each measurement. Set ``inverval`` to 1 for the highest possible resolution in time. Higher integer values downsample the data by measuring every ``interval`` time steps. This can be useful for reducing data storage as needed by the application.\nfields : Tuple[Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'], ...] = ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Collection of field components to store in the monitor.\n\nExample\n-------\n>>> monitor = FieldTimeMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... fields=['Hx'],\n... start=1e-13,\n... stop=5e-13,\n... interval=2,\n... colocate=True,\n... name='movie_monitor')", + "description": ":class:`Monitor` that records electromagnetic fields in the time domain.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included.\ncolocate : bool = True\n Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).\nstart : NonNegativeFloat = 0.0\n [units = sec]. Time at which to start monitor recording.\nstop : Optional[NonNegativeFloat] = None\n [units = sec]. Time at which to stop monitor recording. If not specified, record until end of simulation.\ninterval : Optional[PositiveInt] = None\n Sampling rate of the monitor: number of time steps between each measurement. Set ``inverval`` to 1 for the highest possible resolution in time. Higher integer values downsample the data by measuring every ``interval`` time steps. This can be useful for reducing data storage as needed by the application.\nfields : Tuple[Literal['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz'], ...] = ['Ex', 'Ey', 'Ez', 'Hx', 'Hy', 'Hz']\n Collection of field components to store in the monitor.\n\nExample\n-------\n>>> monitor = FieldTimeMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... fields=['Hx'],\n... start=1e-13,\n... stop=5e-13,\n... interval=2,\n... colocate=True,\n... name='movie_monitor')", "type": "object", "properties": { "type": { @@ -6462,8 +7598,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included.", "default": [ 1, 1, @@ -6488,12 +7624,13 @@ ] }, "colocate": { - "title": "Colocate fields", - "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.", + "title": "Colocate Fields", + "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", + "default": true, "type": "boolean" }, "start": { - "title": "Start time", + "title": "Start Time", "description": "Time at which to start monitor recording.", "default": 0.0, "units": "sec", @@ -6501,14 +7638,14 @@ "type": "number" }, "stop": { - "title": "Stop time", + "title": "Stop Time", "description": "Time at which to stop monitor recording. If not specified, record until end of simulation.", "units": "sec", "minimum": 0, "type": "number" }, "interval": { - "title": "Time interval", + "title": "Time Interval", "description": "Sampling rate of the monitor: number of time steps between each measurement. Set ``inverval`` to 1 for the highest possible resolution in time. Higher integer values downsample the data by measuring every ``interval`` time steps. This can be useful for reducing data storage as needed by the application.", "exclusiveMinimum": 0, "type": "integer" @@ -6546,7 +7683,7 @@ }, "PermittivityMonitor": { "title": "PermittivityMonitor", - "description": ":class:`Monitor` that records the diagonal components of the complex-valued relative\npermittivity tensor in the frequency domain. The recorded data has the same shape as a\n:class:`.FieldMonitor` of the same geometry: the permittivity values are saved at the\nYee grid locations, and can be interpolated to any point inside the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included.\ncolocate : Literal[False] = False\n Colocation turned off, since colocated permittivity values do not have a physical meaning - they do not correspond to the subpixel-averaged ones.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n This field is ignored in this monitor.\n\nNote\n----\nIf 2D materials are present, then the permittivity values correspond to the\nvolumetric equivalent of the 2D materials.\n\nExample\n-------\n>>> monitor = PermittivityMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='eps_monitor')", + "description": ":class:`Monitor` that records the diagonal components of the complex-valued relative\npermittivity tensor in the frequency domain. The recorded data has the same shape as a\n:class:`.FieldMonitor` of the same geometry: the permittivity values are saved at the\nYee grid locations, and can be interpolated to any point inside the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included.\ncolocate : Literal[False] = False\n Colocation turned off, since colocated permittivity values do not have a physical meaning - they do not correspond to the subpixel-averaged ones.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n This field is ignored in this monitor.\n\nNote\n----\nIf 2D materials are present, then the permittivity values correspond to the\nvolumetric equivalent of the 2D materials.\n\nExample\n-------\n>>> monitor = PermittivityMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='eps_monitor')", "type": "object", "properties": { "type": { @@ -6610,8 +7747,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included.", "default": [ 1, 1, @@ -6636,7 +7773,7 @@ ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Colocation turned off, since colocated permittivity values do not have a physical meaning - they do not correspond to the subpixel-averaged ones.", "default": false, "enum": [ @@ -6685,7 +7822,7 @@ }, "FluxMonitor": { "title": "FluxMonitor", - "description": ":class:`Monitor` that records power flux in the frequency domain.\nIf the monitor geometry is a 2D box, the total flux through this plane is returned, with a\npositive sign corresponding to power flow in the positive direction along the axis normal to\nthe plane. If the geometry is a 3D box, the total power coming out of the box is returned by\nintegrating the flux over all box surfaces (excpet the ones defined in ``exclude_surfaces``).\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\n\nExample\n-------\n>>> monitor = FluxMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... freqs=[200e12, 210e12],\n... name='flux_monitor')", + "description": ":class:`Monitor` that records power flux in the frequency domain.\nIf the monitor geometry is a 2D box, the total flux through this plane is returned, with a\npositive sign corresponding to power flow in the positive direction along the axis normal to\nthe plane. If the geometry is a 3D box, the total power coming out of the box is returned by\nintegrating the flux over all box surfaces (excpet the ones defined in ``exclude_surfaces``).\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\n\nExample\n-------\n>>> monitor = FluxMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... freqs=[200e12, 210e12],\n... name='flux_monitor')", "type": "object", "properties": { "type": { @@ -6749,8 +7886,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.", "default": [ 1, 1, @@ -6781,7 +7918,7 @@ ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.", "default": true, "enum": [ @@ -6821,7 +7958,7 @@ ] }, "normal_dir": { - "title": "Normal vector orientation", + "title": "Normal Vector Orientation", "description": "Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.", "enum": [ "+", @@ -6830,7 +7967,7 @@ "type": "string" }, "exclude_surfaces": { - "title": "Excluded surfaces", + "title": "Excluded Surfaces", "description": "Surfaces to exclude in the integration, if a volume monitor.", "type": "array", "items": { @@ -6855,7 +7992,7 @@ }, "FluxTimeMonitor": { "title": "FluxTimeMonitor", - "description": ":class:`Monitor` that records power flux in the time domain.\nIf the monitor geometry is a 2D box, the total flux through this plane is returned, with a\npositive sign corresponding to power flow in the positive direction along the axis normal to\nthe plane. If the geometry is a 3D box, the total power coming out of the box is returned by\nintegrating the flux over all box surfaces (excpet the ones defined in ``exclude_surfaces``).\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nstart : NonNegativeFloat = 0.0\n [units = sec]. Time at which to start monitor recording.\nstop : Optional[NonNegativeFloat] = None\n [units = sec]. Time at which to stop monitor recording. If not specified, record until end of simulation.\ninterval : Optional[PositiveInt] = None\n Sampling rate of the monitor: number of time steps between each measurement. Set ``inverval`` to 1 for the highest possible resolution in time. Higher integer values downsample the data by measuring every ``interval`` time steps. This can be useful for reducing data storage as needed by the application.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\n\nExample\n-------\n>>> monitor = FluxTimeMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... start=1e-13,\n... stop=5e-13,\n... interval=2,\n... name='flux_vs_time')", + "description": ":class:`Monitor` that records power flux in the time domain.\nIf the monitor geometry is a 2D box, the total flux through this plane is returned, with a\npositive sign corresponding to power flow in the positive direction along the axis normal to\nthe plane. If the geometry is a 3D box, the total power coming out of the box is returned by\nintegrating the flux over all box surfaces (excpet the ones defined in ``exclude_surfaces``).\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nstart : NonNegativeFloat = 0.0\n [units = sec]. Time at which to start monitor recording.\nstop : Optional[NonNegativeFloat] = None\n [units = sec]. Time at which to stop monitor recording. If not specified, record until end of simulation.\ninterval : Optional[PositiveInt] = None\n Sampling rate of the monitor: number of time steps between each measurement. Set ``inverval`` to 1 for the highest possible resolution in time. Higher integer values downsample the data by measuring every ``interval`` time steps. This can be useful for reducing data storage as needed by the application.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\n\nExample\n-------\n>>> monitor = FluxTimeMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... start=1e-13,\n... stop=5e-13,\n... interval=2,\n... name='flux_vs_time')", "type": "object", "properties": { "type": { @@ -6919,8 +8056,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.", "default": [ 1, 1, @@ -6951,7 +8088,7 @@ ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.", "default": true, "enum": [ @@ -6960,7 +8097,7 @@ "type": "boolean" }, "start": { - "title": "Start time", + "title": "Start Time", "description": "Time at which to start monitor recording.", "default": 0.0, "units": "sec", @@ -6968,20 +8105,20 @@ "type": "number" }, "stop": { - "title": "Stop time", + "title": "Stop Time", "description": "Time at which to stop monitor recording. If not specified, record until end of simulation.", "units": "sec", "minimum": 0, "type": "number" }, "interval": { - "title": "Time interval", + "title": "Time Interval", "description": "Sampling rate of the monitor: number of time steps between each measurement. Set ``inverval`` to 1 for the highest possible resolution in time. Higher integer values downsample the data by measuring every ``interval`` time steps. This can be useful for reducing data storage as needed by the application.", "exclusiveMinimum": 0, "type": "integer" }, "normal_dir": { - "title": "Normal vector orientation", + "title": "Normal Vector Orientation", "description": "Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.", "enum": [ "+", @@ -6990,7 +8127,7 @@ "type": "string" }, "exclude_surfaces": { - "title": "Excluded surfaces", + "title": "Excluded Surfaces", "description": "Surfaces to exclude in the integration, if a volume monitor.", "type": "array", "items": { @@ -7014,7 +8151,7 @@ }, "ModeMonitor": { "title": "ModeMonitor", - "description": ":class:`Monitor` that records amplitudes from modal decomposition of fields on plane.\nThe amplitudes are defined as\n``mode_solver_data.dot(recorded_field) / mode_solver_data.dot(mode_solver_data)``, where\n``recorded_field`` is the field data recorded in the FDTD simulation at the monitor frequencies,\nand ``mode_solver_data`` is the mode data from the mode solver at the monitor plane.\nThis gives the power amplitude of ``recorded_field`` carried by each mode.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : Literal[False] = False\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nmode_spec : ModeSpec\n Parameters to feed to mode solver which determine modes measured by monitor.\n\nExample\n-------\n>>> mode_spec = ModeSpec(num_modes=3)\n>>> monitor = ModeMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... freqs=[200e12, 210e12],\n... mode_spec=mode_spec,\n... name='mode_monitor')", + "description": ":class:`Monitor` that records amplitudes from modal decomposition of fields on plane.\nThe amplitudes are defined as\n``mode_solver_data.dot(recorded_field) / mode_solver_data.dot(mode_solver_data)``, where\n``recorded_field`` is the field data recorded in the FDTD simulation at the monitor frequencies,\nand ``mode_solver_data`` is the mode data from the mode solver at the monitor plane.\nThis gives the power amplitude of ``recorded_field`` carried by each mode.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.\ncolocate : Literal[False] = False\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nmode_spec : ModeSpec\n Parameters to feed to mode solver which determine modes measured by monitor.\n\nExample\n-------\n>>> mode_spec = ModeSpec(num_modes=3)\n>>> monitor = ModeMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... freqs=[200e12, 210e12],\n... mode_spec=mode_spec,\n... name='mode_monitor')", "type": "object", "properties": { "type": { @@ -7078,8 +8215,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.", "default": [ 1, 1, @@ -7110,7 +8247,7 @@ ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.", "default": false, "enum": [ @@ -7169,7 +8306,7 @@ }, "ModeSolverMonitor": { "title": "ModeSolverMonitor", - "description": ":class:`Monitor` that stores the mode field profiles returned by the mode solver in the\nmonitor plane.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : bool = True\n Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nmode_spec : ModeSpec\n Parameters to feed to mode solver which determine modes measured by monitor.\ndirection : Literal['+', '-'] = +\n Direction of waveguide mode propagation along the axis defined by its normal dimension.\n\nExample\n-------\n>>> mode_spec = ModeSpec(num_modes=3)\n>>> monitor = ModeSolverMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... freqs=[200e12, 210e12],\n... mode_spec=mode_spec,\n... name='mode_monitor')", + "description": ":class:`Monitor` that stores the mode field profiles returned by the mode solver in the\nmonitor plane.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.\ncolocate : bool = True\n Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nmode_spec : ModeSpec\n Parameters to feed to mode solver which determine modes measured by monitor.\ndirection : Literal['+', '-'] = +\n Direction of waveguide mode propagation along the axis defined by its normal dimension.\n\nExample\n-------\n>>> mode_spec = ModeSpec(num_modes=3)\n>>> monitor = ModeSolverMonitor(\n... center=(1,2,3),\n... size=(2,2,0),\n... freqs=[200e12, 210e12],\n... mode_spec=mode_spec,\n... name='mode_monitor')", "type": "object", "properties": { "type": { @@ -7233,8 +8370,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.", "default": [ 1, 1, @@ -7265,8 +8402,8 @@ ] }, "colocate": { - "title": "Colocate fields", - "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.", + "title": "Colocate Fields", + "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", "default": true, "type": "boolean" }, @@ -7311,7 +8448,7 @@ ] }, "direction": { - "title": "Propagation direction", + "title": "Propagation Direction", "description": "Direction of waveguide mode propagation along the axis defined by its normal dimension.", "default": "+", "enum": [ @@ -7331,7 +8468,7 @@ }, "FieldProjectionAngleMonitor": { "title": "FieldProjectionAngleMonitor", - "description": ":class:`Monitor` that samples electromagnetic near fields in the frequency domain\nand projects them at given observation angles. The ``center`` and ``size`` fields define\nwhere the monitor will be placed in order to record near fields, typically very close\nto the structure of interest. The near fields are then projected\nto far-field locations defined by ``phi``, ``theta``, and ``proj_distance``, relative\nto the ``custom_origin``. If the distance between the near and far field locations is\nmuch larger than the size of the device, one can typically set ``far_field_approx`` to\n``True``, which will make use of the far-field approximation to speed up calculations.\nIf the projection distance is comparable to the size of the device, we recommend setting\n``far_field_approx`` to ``False``, so that the approximations are not used, and the\nprojection is accurate even just a few wavelengths away from the near field locations.\nFor applications where the monitor is an open surface rather than a box that\nencloses the device, it is advisable to pick the size of the monitor such that the\nrecorded near fields decay to negligible values near the edges of the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\ncustom_origin : Optional[Tuple[float, float, float]] = None\n [units = um]. Local origin used for defining observation points. If ``None``, uses the monitor's center.\nfar_field_approx : bool = True\n Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.\nproj_distance : float = 1000000.0\n [units = um]. Radial distance of the projection points from ``local_origin``.\ntheta : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = rad]. Polar angles with respect to the global z axis, relative to the location of ``local_origin``, at which to project fields.\nphi : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = rad]. Azimuth angles with respect to the global z axis, relative to the location of ``local_origin``, at which to project fields.\n\nExample\n-------\n>>> monitor = FieldProjectionAngleMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='n2f_monitor',\n... custom_origin=(1,2,3),\n... phi=[0, np.pi/2],\n... theta=np.linspace(-np.pi/2, np.pi/2, 100)\n... )", + "description": ":class:`Monitor` that samples electromagnetic near fields in the frequency domain\nand projects them at given observation angles. The ``center`` and ``size`` fields define\nwhere the monitor will be placed in order to record near fields, typically very close\nto the structure of interest. The near fields are then projected\nto far-field locations defined by ``phi``, ``theta``, and ``proj_distance``, relative\nto the ``custom_origin``. If the distance between the near and far field locations is\nmuch larger than the size of the device, one can typically set ``far_field_approx`` to\n``True``, which will make use of the far-field approximation to speed up calculations.\nIf the projection distance is comparable to the size of the device, we recommend setting\n``far_field_approx`` to ``False``, so that the approximations are not used, and the\nprojection is accurate even just a few wavelengths away from the near field locations.\nFor applications where the monitor is an open surface rather than a box that\nencloses the device, it is advisable to pick the size of the monitor such that the\nrecorded near fields decay to negligible values near the edges of the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals at which near fields are recorded for projection to the far field, along each direction. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Using values greater than 1 can help speed up server-side far field projections with minimal accuracy loss, especially in cases where it is necessary for the grid resolution to be high for the FDTD simulation, but such a high resolution is unnecessary for the purpose of projecting the recorded near fields to the far field.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\ncustom_origin : Optional[Tuple[float, float, float]] = None\n [units = um]. Local origin used for defining observation points. If ``None``, uses the monitor's center.\nfar_field_approx : bool = True\n Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.\nwindow_size : Tuple[NonNegativeFloat, NonNegativeFloat] = (0, 0)\n Size of the transition region of the windowing function used to ensure that the recorded near fields decay to zero near the edges of the monitor. The two components refer to the two tangential directions associated with each surface. For surfaces with the normal along ``x``, the two components are (``y``, ``z``). For surfaces with the normal along ``y``, the two components are (``x``, ``z``). For surfaces with the normal along ``z``, the two components are (``x``, ``y``). Each value must be between 0 and 1, inclusive, and denotes the size of the transition region over which fields are scaled to less than a thousandth of the original amplitude, relative to half the size of the monitor in that direction. A value of 0 turns windowing off in that direction, while a value of 1 indicates that the window will be applied to the entire monitor in that direction. This field is applicable for surface monitors only, and otherwise must remain (0, 0).\nmedium : Union[Medium, AnisotropicMedium, PECMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, FullyAnisotropicMedium, CustomMedium, CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomAnisotropicMedium, PerturbationMedium, PerturbationPoleResidue, Medium2D] = None\n Medium through which to project fields. Generally, the fields should be projected through the same medium as the one in which this monitor is placed, and this is the default behavior when ``medium=None``. A custom ``medium`` can be useful in some situations for advanced users, but we recommend trying to avoid using a non-default ``medium``.\nproj_distance : float = 1000000.0\n [units = um]. Radial distance of the projection points from ``local_origin``.\ntheta : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = rad]. Polar angles with respect to the global z axis, relative to the location of ``local_origin``, at which to project fields.\nphi : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = rad]. Azimuth angles with respect to the global z axis, relative to the location of ``local_origin``, at which to project fields.\n\nExample\n-------\n>>> monitor = FieldProjectionAngleMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='n2f_monitor',\n... custom_origin=(1,2,3),\n... phi=[0, np.pi/2],\n... theta=np.linspace(-np.pi/2, np.pi/2, 100)\n... )", "type": "object", "properties": { "type": { @@ -7395,8 +8532,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals at which near fields are recorded for projection to the far field, along each direction. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Using values greater than 1 can help speed up server-side far field projections with minimal accuracy loss, especially in cases where it is necessary for the grid resolution to be high for the FDTD simulation, but such a high resolution is unnecessary for the purpose of projecting the recorded near fields to the far field.", "default": [ 1, 1, @@ -7407,27 +8544,21 @@ "maxItems": 3, "items": [ { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 }, { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 }, { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 } ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.", "default": true, "enum": [ @@ -7462,69 +8593,153 @@ }, "allOf": [ { - "$ref": "#/definitions/ApodizationSpec" - } - ] - }, - "normal_dir": { - "title": "Normal vector orientation", - "description": "Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.", - "enum": [ - "+", - "-" - ], - "type": "string" - }, - "exclude_surfaces": { - "title": "Excluded surfaces", - "description": "Surfaces to exclude in the integration, if a volume monitor.", - "type": "array", - "items": { - "enum": [ - "x-", - "x+", - "y-", - "y+", - "z-", - "z+" - ], - "type": "string" - } - }, - "custom_origin": { - "title": "Local origin", - "description": "Local origin used for defining observation points. If ``None``, uses the monitor's center.", - "units": "um", - "type": "array", - "minItems": 3, - "maxItems": 3, - "items": [ + "$ref": "#/definitions/ApodizationSpec" + } + ] + }, + "normal_dir": { + "title": "Normal Vector Orientation", + "description": "Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.", + "enum": [ + "+", + "-" + ], + "type": "string" + }, + "exclude_surfaces": { + "title": "Excluded Surfaces", + "description": "Surfaces to exclude in the integration, if a volume monitor.", + "type": "array", + "items": { + "enum": [ + "x-", + "x+", + "y-", + "y+", + "z-", + "z+" + ], + "type": "string" + } + }, + "custom_origin": { + "title": "Local Origin", + "description": "Local origin used for defining observation points. If ``None``, uses the monitor's center.", + "units": "um", + "type": "array", + "minItems": 3, + "maxItems": 3, + "items": [ + { + "type": "number" + }, + { + "type": "number" + }, + { + "type": "number" + } + ] + }, + "far_field_approx": { + "title": "Far Field Approximation", + "description": "Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.", + "default": true, + "type": "boolean" + }, + "window_size": { + "title": "Spatial filtering window size", + "description": "Size of the transition region of the windowing function used to ensure that the recorded near fields decay to zero near the edges of the monitor. The two components refer to the two tangential directions associated with each surface. For surfaces with the normal along ``x``, the two components are (``y``, ``z``). For surfaces with the normal along ``y``, the two components are (``x``, ``z``). For surfaces with the normal along ``z``, the two components are (``x``, ``y``). Each value must be between 0 and 1, inclusive, and denotes the size of the transition region over which fields are scaled to less than a thousandth of the original amplitude, relative to half the size of the monitor in that direction. A value of 0 turns windowing off in that direction, while a value of 1 indicates that the window will be applied to the entire monitor in that direction. This field is applicable for surface monitors only, and otherwise must remain (0, 0).", + "default": [ + 0, + 0 + ], + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0 + } + ] + }, + "medium": { + "title": "Projection medium", + "description": "Medium through which to project fields. Generally, the fields should be projected through the same medium as the one in which this monitor is placed, and this is the default behavior when ``medium=None``. A custom ``medium`` can be useful in some situations for advanced users, but we recommend trying to avoid using a non-default ``medium``.", + "anyOf": [ + { + "$ref": "#/definitions/Medium" + }, + { + "$ref": "#/definitions/AnisotropicMedium" + }, + { + "$ref": "#/definitions/PECMedium" + }, + { + "$ref": "#/definitions/PoleResidue" + }, + { + "$ref": "#/definitions/Sellmeier" + }, + { + "$ref": "#/definitions/Lorentz" + }, + { + "$ref": "#/definitions/Debye" + }, + { + "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/FullyAnisotropicMedium" + }, + { + "$ref": "#/definitions/CustomMedium" + }, + { + "$ref": "#/definitions/CustomPoleResidue" + }, + { + "$ref": "#/definitions/CustomSellmeier" + }, { - "type": "number" + "$ref": "#/definitions/CustomLorentz" }, { - "type": "number" + "$ref": "#/definitions/CustomDebye" }, { - "type": "number" + "$ref": "#/definitions/CustomDrude" + }, + { + "$ref": "#/definitions/CustomAnisotropicMedium" + }, + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + }, + { + "$ref": "#/definitions/Medium2D" } ] }, - "far_field_approx": { - "title": "Far field approximation", - "description": "Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.", - "default": true, - "type": "boolean" - }, "proj_distance": { - "title": "Projection distance", + "title": "Projection Distance", "description": "Radial distance of the projection points from ``local_origin``.", "default": 1000000.0, "units": "um", "type": "number" }, "theta": { - "title": "Polar angles", + "title": "Polar Angles", "description": "Polar angles with respect to the global z axis, relative to the location of ``local_origin``, at which to project fields.", "units": "rad", "anyOf": [ @@ -7540,7 +8755,7 @@ ] }, "phi": { - "title": "Azimuth angles", + "title": "Azimuth Angles", "description": "Azimuth angles with respect to the global z axis, relative to the location of ``local_origin``, at which to project fields.", "units": "rad", "anyOf": [ @@ -7567,7 +8782,7 @@ }, "FieldProjectionCartesianMonitor": { "title": "FieldProjectionCartesianMonitor", - "description": ":class:`Monitor` that samples electromagnetic near fields in the frequency domain\nand projects them on a Cartesian observation plane. The ``center`` and ``size`` fields define\nwhere the monitor will be placed in order to record near fields, typically very close\nto the structure of interest. The near fields are then projected\nto far-field locations defined by ``x``, ``y``, and ``proj_distance``, relative\nto the ``custom_origin``. Here, ``x`` and ``y`` correspond to a local coordinate system\nwhere the local z axis is defined by ``proj_axis``: which is the axis normal to this monitor.\nIf the distance between the near and far field locations is much larger than the size of the\ndevice, one can typically set ``far_field_approx`` to ``True``, which will make use of the\nfar-field approximation to speed up calculations. If the projection distance is comparable\nto the size of the device, we recommend setting ``far_field_approx`` to ``False``,\nso that the approximations are not used, and the projection is accurate even just a few\nwavelengths away from the near field locations.\nFor applications where the monitor is an open surface rather than a box that\nencloses the device, it is advisable to pick the size of the monitor such that the\nrecorded near fields decay to negligible values near the edges of the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\ncustom_origin : Optional[Tuple[float, float, float]] = None\n [units = um]. Local origin used for defining observation points. If ``None``, uses the monitor's center.\nfar_field_approx : bool = True\n Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.\nproj_axis : Literal[0, 1, 2]\n Axis along which the observation plane is oriented.\nproj_distance : float = 1000000.0\n [units = um]. Signed distance of the projection plane along ``proj_axis``. from the plane containing ``local_origin``.\nx : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = um]. Local x observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. When ``proj_axis`` is 0, this corresponds to the global y axis. When ``proj_axis`` is 1, this corresponds to the global x axis. When ``proj_axis`` is 2, this corresponds to the global x axis. \ny : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = um]. Local y observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. When ``proj_axis`` is 0, this corresponds to the global z axis. When ``proj_axis`` is 1, this corresponds to the global z axis. When ``proj_axis`` is 2, this corresponds to the global y axis. \n\nExample\n-------\n>>> monitor = FieldProjectionCartesianMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='n2f_monitor',\n... custom_origin=(1,2,3),\n... x=[-1, 0, 1],\n... y=[-2, -1, 0, 1, 2],\n... proj_axis=2,\n... proj_distance=5\n... )", + "description": ":class:`Monitor` that samples electromagnetic near fields in the frequency domain\nand projects them on a Cartesian observation plane. The ``center`` and ``size`` fields define\nwhere the monitor will be placed in order to record near fields, typically very close\nto the structure of interest. The near fields are then projected\nto far-field locations defined by ``x``, ``y``, and ``proj_distance``, relative\nto the ``custom_origin``. Here, ``x`` and ``y`` correspond to a local coordinate system\nwhere the local z axis is defined by ``proj_axis``: which is the axis normal to this monitor.\nIf the distance between the near and far field locations is much larger than the size of the\ndevice, one can typically set ``far_field_approx`` to ``True``, which will make use of the\nfar-field approximation to speed up calculations. If the projection distance is comparable\nto the size of the device, we recommend setting ``far_field_approx`` to ``False``,\nso that the approximations are not used, and the projection is accurate even just a few\nwavelengths away from the near field locations.\nFor applications where the monitor is an open surface rather than a box that\nencloses the device, it is advisable to pick the size of the monitor such that the\nrecorded near fields decay to negligible values near the edges of the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals at which near fields are recorded for projection to the far field, along each direction. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Using values greater than 1 can help speed up server-side far field projections with minimal accuracy loss, especially in cases where it is necessary for the grid resolution to be high for the FDTD simulation, but such a high resolution is unnecessary for the purpose of projecting the recorded near fields to the far field.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\ncustom_origin : Optional[Tuple[float, float, float]] = None\n [units = um]. Local origin used for defining observation points. If ``None``, uses the monitor's center.\nfar_field_approx : bool = True\n Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.\nwindow_size : Tuple[NonNegativeFloat, NonNegativeFloat] = (0, 0)\n Size of the transition region of the windowing function used to ensure that the recorded near fields decay to zero near the edges of the monitor. The two components refer to the two tangential directions associated with each surface. For surfaces with the normal along ``x``, the two components are (``y``, ``z``). For surfaces with the normal along ``y``, the two components are (``x``, ``z``). For surfaces with the normal along ``z``, the two components are (``x``, ``y``). Each value must be between 0 and 1, inclusive, and denotes the size of the transition region over which fields are scaled to less than a thousandth of the original amplitude, relative to half the size of the monitor in that direction. A value of 0 turns windowing off in that direction, while a value of 1 indicates that the window will be applied to the entire monitor in that direction. This field is applicable for surface monitors only, and otherwise must remain (0, 0).\nmedium : Union[Medium, AnisotropicMedium, PECMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, FullyAnisotropicMedium, CustomMedium, CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomAnisotropicMedium, PerturbationMedium, PerturbationPoleResidue, Medium2D] = None\n Medium through which to project fields. Generally, the fields should be projected through the same medium as the one in which this monitor is placed, and this is the default behavior when ``medium=None``. A custom ``medium`` can be useful in some situations for advanced users, but we recommend trying to avoid using a non-default ``medium``.\nproj_axis : Literal[0, 1, 2]\n Axis along which the observation plane is oriented.\nproj_distance : float = 1000000.0\n [units = um]. Signed distance of the projection plane along ``proj_axis``. from the plane containing ``local_origin``.\nx : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = um]. Local x observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. When ``proj_axis`` is 0, this corresponds to the global y axis. When ``proj_axis`` is 1, this corresponds to the global x axis. When ``proj_axis`` is 2, this corresponds to the global x axis. \ny : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = um]. Local y observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. When ``proj_axis`` is 0, this corresponds to the global z axis. When ``proj_axis`` is 1, this corresponds to the global z axis. When ``proj_axis`` is 2, this corresponds to the global y axis. \n\nExample\n-------\n>>> monitor = FieldProjectionCartesianMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='n2f_monitor',\n... custom_origin=(1,2,3),\n... x=[-1, 0, 1],\n... y=[-2, -1, 0, 1, 2],\n... proj_axis=2,\n... proj_distance=5\n... )", "type": "object", "properties": { "type": { @@ -7631,8 +8846,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals at which near fields are recorded for projection to the far field, along each direction. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Using values greater than 1 can help speed up server-side far field projections with minimal accuracy loss, especially in cases where it is necessary for the grid resolution to be high for the FDTD simulation, but such a high resolution is unnecessary for the purpose of projecting the recorded near fields to the far field.", "default": [ 1, 1, @@ -7643,27 +8858,21 @@ "maxItems": 3, "items": [ { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 }, { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 }, { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 } ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.", "default": true, "enum": [ @@ -7703,7 +8912,7 @@ ] }, "normal_dir": { - "title": "Normal vector orientation", + "title": "Normal Vector Orientation", "description": "Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.", "enum": [ "+", @@ -7712,7 +8921,7 @@ "type": "string" }, "exclude_surfaces": { - "title": "Excluded surfaces", + "title": "Excluded Surfaces", "description": "Surfaces to exclude in the integration, if a volume monitor.", "type": "array", "items": { @@ -7728,7 +8937,7 @@ } }, "custom_origin": { - "title": "Local origin", + "title": "Local Origin", "description": "Local origin used for defining observation points. If ``None``, uses the monitor's center.", "units": "um", "type": "array", @@ -7747,13 +8956,97 @@ ] }, "far_field_approx": { - "title": "Far field approximation", + "title": "Far Field Approximation", "description": "Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.", "default": true, "type": "boolean" }, + "window_size": { + "title": "Spatial filtering window size", + "description": "Size of the transition region of the windowing function used to ensure that the recorded near fields decay to zero near the edges of the monitor. The two components refer to the two tangential directions associated with each surface. For surfaces with the normal along ``x``, the two components are (``y``, ``z``). For surfaces with the normal along ``y``, the two components are (``x``, ``z``). For surfaces with the normal along ``z``, the two components are (``x``, ``y``). Each value must be between 0 and 1, inclusive, and denotes the size of the transition region over which fields are scaled to less than a thousandth of the original amplitude, relative to half the size of the monitor in that direction. A value of 0 turns windowing off in that direction, while a value of 1 indicates that the window will be applied to the entire monitor in that direction. This field is applicable for surface monitors only, and otherwise must remain (0, 0).", + "default": [ + 0, + 0 + ], + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0 + } + ] + }, + "medium": { + "title": "Projection medium", + "description": "Medium through which to project fields. Generally, the fields should be projected through the same medium as the one in which this monitor is placed, and this is the default behavior when ``medium=None``. A custom ``medium`` can be useful in some situations for advanced users, but we recommend trying to avoid using a non-default ``medium``.", + "anyOf": [ + { + "$ref": "#/definitions/Medium" + }, + { + "$ref": "#/definitions/AnisotropicMedium" + }, + { + "$ref": "#/definitions/PECMedium" + }, + { + "$ref": "#/definitions/PoleResidue" + }, + { + "$ref": "#/definitions/Sellmeier" + }, + { + "$ref": "#/definitions/Lorentz" + }, + { + "$ref": "#/definitions/Debye" + }, + { + "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/FullyAnisotropicMedium" + }, + { + "$ref": "#/definitions/CustomMedium" + }, + { + "$ref": "#/definitions/CustomPoleResidue" + }, + { + "$ref": "#/definitions/CustomSellmeier" + }, + { + "$ref": "#/definitions/CustomLorentz" + }, + { + "$ref": "#/definitions/CustomDebye" + }, + { + "$ref": "#/definitions/CustomDrude" + }, + { + "$ref": "#/definitions/CustomAnisotropicMedium" + }, + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + }, + { + "$ref": "#/definitions/Medium2D" + } + ] + }, "proj_axis": { - "title": "Projection plane axis", + "title": "Projection Plane Axis", "description": "Axis along which the observation plane is oriented.", "enum": [ 0, @@ -7763,14 +9056,14 @@ "type": "integer" }, "proj_distance": { - "title": "Projection distance", + "title": "Projection Distance", "description": "Signed distance of the projection plane along ``proj_axis``. from the plane containing ``local_origin``.", "default": 1000000.0, "units": "um", "type": "number" }, "x": { - "title": "Local x observation coordinates", + "title": "Local x Observation Coordinates", "description": "Local x observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. When ``proj_axis`` is 0, this corresponds to the global y axis. When ``proj_axis`` is 1, this corresponds to the global x axis. When ``proj_axis`` is 2, this corresponds to the global x axis. ", "units": "um", "anyOf": [ @@ -7786,7 +9079,7 @@ ] }, "y": { - "title": "Local y observation coordinates", + "title": "Local y Observation Coordinates", "description": "Local y observation coordinates w.r.t. ``local_origin`` and ``proj_axis``. When ``proj_axis`` is 0, this corresponds to the global z axis. When ``proj_axis`` is 1, this corresponds to the global z axis. When ``proj_axis`` is 2, this corresponds to the global y axis. ", "units": "um", "anyOf": [ @@ -7814,7 +9107,7 @@ }, "FieldProjectionKSpaceMonitor": { "title": "FieldProjectionKSpaceMonitor", - "description": ":class:`Monitor` that samples electromagnetic near fields in the frequency domain\nand projects them on an observation plane defined in k-space. The ``center`` and ``size``\nfields define where the monitor will be placed in order to record near fields, typically\nvery close to the structure of interest. The near fields are then\nprojected to far-field locations defined in k-space by ``ux``, ``uy``, and ``proj_distance``,\nrelative to the ``custom_origin``. Here, ``ux`` and ``uy`` are associated with a local\ncoordinate system where the local 'z' axis is defined by ``proj_axis``: which is the axis\nnormal to this monitor. If the distance between the near and far field locations is much\nlarger than the size of the device, one can typically set ``far_field_approx`` to ``True``,\nwhich will make use of the far-field approximation to speed up calculations. If the\nprojection distance is comparable to the size of the device, we recommend setting\n``far_field_approx`` to ``False``, so that the approximations are not used, and the\nprojection is accurate even just a few wavelengths away from the near field locations.\nFor applications where the monitor is an open surface rather than a box that\nencloses the device, it is advisable to pick the size of the monitor such that the\nrecorded near fields decay to negligible values near the edges of the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\ncustom_origin : Optional[Tuple[float, float, float]] = None\n [units = um]. Local origin used for defining observation points. If ``None``, uses the monitor's center.\nfar_field_approx : bool = True\n Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.\nproj_axis : Literal[0, 1, 2]\n Axis along which the observation plane is oriented.\nproj_distance : float = 1000000.0\n [units = um]. Radial distance of the projection points from ``local_origin``.\nux : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n Local x component of wave vectors on the observation plane, relative to ``local_origin`` and oriented with respect to ``proj_axis``, normalized by (2*pi/lambda) where lambda is the wavelength associated with the background medium. Must be in the range [-1, 1].\nuy : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n Local y component of wave vectors on the observation plane, relative to ``local_origin`` and oriented with respect to ``proj_axis``, normalized by (2*pi/lambda) where lambda is the wavelength associated with the background medium. Must be in the range [-1, 1].\n\nExample\n-------\n>>> monitor = FieldProjectionKSpaceMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='n2f_monitor',\n... custom_origin=(1,2,3),\n... proj_axis=2,\n... ux=[0.1,0.2],\n... uy=[0.3,0.4,0.5]\n... )", + "description": ":class:`Monitor` that samples electromagnetic near fields in the frequency domain\nand projects them on an observation plane defined in k-space. The ``center`` and ``size``\nfields define where the monitor will be placed in order to record near fields, typically\nvery close to the structure of interest. The near fields are then\nprojected to far-field locations defined in k-space by ``ux``, ``uy``, and ``proj_distance``,\nrelative to the ``custom_origin``. Here, ``ux`` and ``uy`` are associated with a local\ncoordinate system where the local 'z' axis is defined by ``proj_axis``: which is the axis\nnormal to this monitor. If the distance between the near and far field locations is much\nlarger than the size of the device, one can typically set ``far_field_approx`` to ``True``,\nwhich will make use of the far-field approximation to speed up calculations. If the\nprojection distance is comparable to the size of the device, we recommend setting\n``far_field_approx`` to ``False``, so that the approximations are not used, and the\nprojection is accurate even just a few wavelengths away from the near field locations.\nFor applications where the monitor is an open surface rather than a box that\nencloses the device, it is advisable to pick the size of the monitor such that the\nrecorded near fields decay to negligible values near the edges of the monitor.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[PositiveInt, PositiveInt, PositiveInt] = (1, 1, 1)\n Number of grid step intervals at which near fields are recorded for projection to the far field, along each direction. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Using values greater than 1 can help speed up server-side far field projections with minimal accuracy loss, especially in cases where it is necessary for the grid resolution to be high for the FDTD simulation, but such a high resolution is unnecessary for the purpose of projecting the recorded near fields to the far field.\ncolocate : Literal[True] = True\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Optional[Literal['+', '-']] = None\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.\nexclude_surfaces : Optional[Tuple[Literal['x-', 'x+', 'y-', 'y+', 'z-', 'z+'], ...]] = None\n Surfaces to exclude in the integration, if a volume monitor.\ncustom_origin : Optional[Tuple[float, float, float]] = None\n [units = um]. Local origin used for defining observation points. If ``None``, uses the monitor's center.\nfar_field_approx : bool = True\n Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.\nwindow_size : Tuple[NonNegativeFloat, NonNegativeFloat] = (0, 0)\n Size of the transition region of the windowing function used to ensure that the recorded near fields decay to zero near the edges of the monitor. The two components refer to the two tangential directions associated with each surface. For surfaces with the normal along ``x``, the two components are (``y``, ``z``). For surfaces with the normal along ``y``, the two components are (``x``, ``z``). For surfaces with the normal along ``z``, the two components are (``x``, ``y``). Each value must be between 0 and 1, inclusive, and denotes the size of the transition region over which fields are scaled to less than a thousandth of the original amplitude, relative to half the size of the monitor in that direction. A value of 0 turns windowing off in that direction, while a value of 1 indicates that the window will be applied to the entire monitor in that direction. This field is applicable for surface monitors only, and otherwise must remain (0, 0).\nmedium : Union[Medium, AnisotropicMedium, PECMedium, PoleResidue, Sellmeier, Lorentz, Debye, Drude, FullyAnisotropicMedium, CustomMedium, CustomPoleResidue, CustomSellmeier, CustomLorentz, CustomDebye, CustomDrude, CustomAnisotropicMedium, PerturbationMedium, PerturbationPoleResidue, Medium2D] = None\n Medium through which to project fields. Generally, the fields should be projected through the same medium as the one in which this monitor is placed, and this is the default behavior when ``medium=None``. A custom ``medium`` can be useful in some situations for advanced users, but we recommend trying to avoid using a non-default ``medium``.\nproj_axis : Literal[0, 1, 2]\n Axis along which the observation plane is oriented.\nproj_distance : float = 1000000.0\n [units = um]. Radial distance of the projection points from ``local_origin``.\nux : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n Local x component of wave vectors on the observation plane, relative to ``local_origin`` and oriented with respect to ``proj_axis``, normalized by (2*pi/lambda) where lambda is the wavelength associated with the background medium. Must be in the range [-1, 1].\nuy : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n Local y component of wave vectors on the observation plane, relative to ``local_origin`` and oriented with respect to ``proj_axis``, normalized by (2*pi/lambda) where lambda is the wavelength associated with the background medium. Must be in the range [-1, 1].\n\nExample\n-------\n>>> monitor = FieldProjectionKSpaceMonitor(\n... center=(1,2,3),\n... size=(2,2,2),\n... freqs=[250e12, 300e12],\n... name='n2f_monitor',\n... custom_origin=(1,2,3),\n... proj_axis=2,\n... ux=[0.1,0.2],\n... uy=[0.3,0.4,0.5]\n... )", "type": "object", "properties": { "type": { @@ -7878,8 +9171,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals at which near fields are recorded for projection to the far field, along each direction. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Using values greater than 1 can help speed up server-side far field projections with minimal accuracy loss, especially in cases where it is necessary for the grid resolution to be high for the FDTD simulation, but such a high resolution is unnecessary for the purpose of projecting the recorded near fields to the far field.", "default": [ 1, 1, @@ -7890,27 +9183,21 @@ "maxItems": 3, "items": [ { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 }, { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 }, { - "enum": [ - 1 - ], - "type": "integer" + "type": "integer", + "exclusiveMinimum": 0 } ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.", "default": true, "enum": [ @@ -7950,7 +9237,7 @@ ] }, "normal_dir": { - "title": "Normal vector orientation", + "title": "Normal Vector Orientation", "description": "Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Applies to surface monitors only, and defaults to ``'+'`` if not provided.", "enum": [ "+", @@ -7959,7 +9246,7 @@ "type": "string" }, "exclude_surfaces": { - "title": "Excluded surfaces", + "title": "Excluded Surfaces", "description": "Surfaces to exclude in the integration, if a volume monitor.", "type": "array", "items": { @@ -7975,7 +9262,7 @@ } }, "custom_origin": { - "title": "Local origin", + "title": "Local Origin", "description": "Local origin used for defining observation points. If ``None``, uses the monitor's center.", "units": "um", "type": "array", @@ -7994,13 +9281,97 @@ ] }, "far_field_approx": { - "title": "Far field approximation", + "title": "Far Field Approximation", "description": "Whether to enable the far field approximation when projecting fields. If ``True``, terms that decay as O(1/r^2) are ignored, as are the radial components of fields. Typically, this should be set to ``True`` only when the projection distance is much larger than the size of the device being modeled, and the projected points are in the far field of the device.", "default": true, "type": "boolean" }, + "window_size": { + "title": "Spatial filtering window size", + "description": "Size of the transition region of the windowing function used to ensure that the recorded near fields decay to zero near the edges of the monitor. The two components refer to the two tangential directions associated with each surface. For surfaces with the normal along ``x``, the two components are (``y``, ``z``). For surfaces with the normal along ``y``, the two components are (``x``, ``z``). For surfaces with the normal along ``z``, the two components are (``x``, ``y``). Each value must be between 0 and 1, inclusive, and denotes the size of the transition region over which fields are scaled to less than a thousandth of the original amplitude, relative to half the size of the monitor in that direction. A value of 0 turns windowing off in that direction, while a value of 1 indicates that the window will be applied to the entire monitor in that direction. This field is applicable for surface monitors only, and otherwise must remain (0, 0).", + "default": [ + 0, + 0 + ], + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "number", + "minimum": 0 + }, + { + "type": "number", + "minimum": 0 + } + ] + }, + "medium": { + "title": "Projection medium", + "description": "Medium through which to project fields. Generally, the fields should be projected through the same medium as the one in which this monitor is placed, and this is the default behavior when ``medium=None``. A custom ``medium`` can be useful in some situations for advanced users, but we recommend trying to avoid using a non-default ``medium``.", + "anyOf": [ + { + "$ref": "#/definitions/Medium" + }, + { + "$ref": "#/definitions/AnisotropicMedium" + }, + { + "$ref": "#/definitions/PECMedium" + }, + { + "$ref": "#/definitions/PoleResidue" + }, + { + "$ref": "#/definitions/Sellmeier" + }, + { + "$ref": "#/definitions/Lorentz" + }, + { + "$ref": "#/definitions/Debye" + }, + { + "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/FullyAnisotropicMedium" + }, + { + "$ref": "#/definitions/CustomMedium" + }, + { + "$ref": "#/definitions/CustomPoleResidue" + }, + { + "$ref": "#/definitions/CustomSellmeier" + }, + { + "$ref": "#/definitions/CustomLorentz" + }, + { + "$ref": "#/definitions/CustomDebye" + }, + { + "$ref": "#/definitions/CustomDrude" + }, + { + "$ref": "#/definitions/CustomAnisotropicMedium" + }, + { + "$ref": "#/definitions/PerturbationMedium" + }, + { + "$ref": "#/definitions/PerturbationPoleResidue" + }, + { + "$ref": "#/definitions/Medium2D" + } + ] + }, "proj_axis": { - "title": "Projection plane axis", + "title": "Projection Plane Axis", "description": "Axis along which the observation plane is oriented.", "enum": [ 0, @@ -8010,7 +9381,7 @@ "type": "integer" }, "proj_distance": { - "title": "Projection distance", + "title": "Projection Distance", "description": "Radial distance of the projection points from ``local_origin``.", "default": 1000000.0, "units": "um", @@ -8059,7 +9430,7 @@ }, "DiffractionMonitor": { "title": "DiffractionMonitor", - "description": ":class:`Monitor` that uses a 2D Fourier transform to compute the\ndiffraction amplitudes and efficiency for allowed diffraction orders.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.\ncolocate : Literal[False] = False\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Literal['+', '-'] = +\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Defaults to ``'+'`` if not provided.\n\nExample\n-------\n>>> monitor = DiffractionMonitor(\n... center=(1,2,3),\n... size=(inf,inf,0),\n... freqs=[250e12, 300e12],\n... name='diffraction_monitor',\n... normal_dir='+',\n... )", + "description": ":class:`Monitor` that uses a 2D Fourier transform to compute the\ndiffraction amplitudes and efficiency for allowed diffraction orders.\n\nParameters\n----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[NonNegativeFloat, NonNegativeFloat, NonNegativeFloat]\n [units = um]. Size in x, y, and z directions.\nname : ConstrainedStrValue\n Unique name for monitor.\ninterval_space : Tuple[Literal[1], Literal[1], Literal[1]] = (1, 1, 1)\n Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.\ncolocate : Literal[False] = False\n Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.\nfreqs : Union[Tuple[float, ...], ArrayLike[dtype=float, ndim=1]]\n [units = Hz]. Array or list of frequencies stored by the field monitor.\napodization : ApodizationSpec = ApodizationSpec(start=None, end=None, width=None, type='ApodizationSpec')\n Sets parameters of (optional) apodization. Apodization applies a windowing function to the Fourier transform of the time-domain fields into frequency-domain ones, and can be used to truncate the beginning and/or end of the time signal, for example to eliminate the source pulse when studying the eigenmodes of a system. Note: apodization affects the normalization of the frequency-domain fields.\nnormal_dir : Literal['+', '-'] = +\n Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Defaults to ``'+'`` if not provided.\n\nExample\n-------\n>>> monitor = DiffractionMonitor(\n... center=(1,2,3),\n... size=(inf,inf,0),\n... freqs=[250e12, 300e12],\n... name='diffraction_monitor',\n... normal_dir='+',\n... )", "type": "object", "properties": { "type": { @@ -8123,8 +9494,8 @@ "type": "string" }, "interval_space": { - "title": "Spatial interval", - "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the last point of the monitor grid is always included. Not all monitors support values different from 1.", + "title": "Spatial Interval", + "description": "Number of grid step intervals between monitor recordings. If equal to 1, there will be no downsampling. If greater than 1, the step will be applied, but the first and last point of the monitor grid are always included. Not all monitors support values different from 1.", "default": [ 1, 1, @@ -8155,7 +9526,7 @@ ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Defines whether fields are colocated to grid cell boundaries (i.e. to the primal grid) on-the-fly during a solver run. Can be toggled for field recording monitors and is hard-coded for other monitors depending on their specific function.", "default": false, "enum": [ @@ -8195,7 +9566,7 @@ ] }, "normal_dir": { - "title": "Normal vector orientation", + "title": "Normal Vector Orientation", "description": "Direction of the surface monitor's normal vector w.r.t. the positive x, y or z unit vectors. Must be one of ``'+'`` or ``'-'``. Defaults to ``'+'`` if not provided.", "default": "+", "enum": [ @@ -8341,7 +9712,7 @@ }, "MeshOverrideStructure": { "title": "MeshOverrideStructure", - "description": "Defines an object that is only used in the process of generating the mesh.\nA :class:`MeshOverrideStructure` is a combination of geometry :class:`Geometry`,\ngrid size along x,y,z directions, and a boolean on whether the override\nwill be enforced.\n\nParameters\n----------\ngeometry : Union[Box, ClipOperation, GeometryGroup, Sphere, Cylinder, PolySlab, ComplexPolySlabBase, TriangleMesh]\n Defines geometric properties of the structure.\nname : Optional[str] = None\n Optional name for the structure.\ndl : Tuple[PositiveFloat, PositiveFloat, PositiveFloat]\n [units = um]. Grid size along x, y, z directions.\nenforce : bool = False\n If ``True``, enforce the grid size setup inside the structure even if the structure is inside a structure of smaller grid size. In the intersection region of multiple structures of ``enforce=True``, grid size is decided by the last added structure of ``enforce=True``.\n\nExample\n-------\n>>> from tidy3d import Box\n>>> box = Box(center=(0,0,1), size=(2, 2, 2))\n>>> struct_override = MeshOverrideStructure(geometry=box, dl=(0.1,0.2,0.3), name='override_box')", + "description": "Defines an object that is only used in the process of generating the mesh.\nA :class:`MeshOverrideStructure` is a combination of geometry :class:`Geometry`,\ngrid size along x,y,z directions, and a boolean on whether the override\nwill be enforced.\n\nParameters\n----------\ngeometry : Union[Box, Transformed, ClipOperation, GeometryGroup, Sphere, Cylinder, PolySlab, ComplexPolySlabBase, TriangleMesh]\n Defines geometric properties of the structure.\nname : Optional[str] = None\n Optional name for the structure.\ndl : Tuple[PositiveFloat, PositiveFloat, PositiveFloat]\n [units = um]. Grid size along x, y, z directions.\nenforce : bool = False\n If ``True``, enforce the grid size setup inside the structure even if the structure is inside a structure of smaller grid size. In the intersection region of multiple structures of ``enforce=True``, grid size is decided by the last added structure of ``enforce=True``.\n\nExample\n-------\n>>> from tidy3d import Box\n>>> box = Box(center=(0,0,1), size=(2, 2, 2))\n>>> struct_override = MeshOverrideStructure(geometry=box, dl=(0.1,0.2,0.3), name='override_box')", "type": "object", "properties": { "geometry": { @@ -8351,6 +9722,7 @@ "propertyName": "type", "mapping": { "Box": "#/definitions/Box", + "Transformed": "#/definitions/Transformed", "ClipOperation": "#/definitions/ClipOperation", "GeometryGroup": "#/definitions/GeometryGroup", "Sphere": "#/definitions/Sphere", @@ -8364,6 +9736,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, diff --git a/tidy3d/version.py b/tidy3d/version.py index 98ef4b309..2cc89c4ac 100644 --- a/tidy3d/version.py +++ b/tidy3d/version.py @@ -1,3 +1,3 @@ """Defines the front end version of tidy3d""" -__version__ = "2.4.3" +__version__ = "2.5.0" diff --git a/tidy3d/web/__init__.py b/tidy3d/web/__init__.py index 2557b08e4..f0e23645f 100644 --- a/tidy3d/web/__init__.py +++ b/tidy3d/web/__init__.py @@ -1,6 +1,8 @@ """ imports interfaces for interacting with server """ +from .api.container import Job, Batch, BatchData from .cli.migrate import migrate -from .webapi import ( +from .cli.app import configure_fn as configure +from .api.webapi import ( run, upload, get_info, @@ -11,16 +13,24 @@ load, estimate_cost, abort, + get_tasks, + delete_old, + download_log, + download_json, + load_simulation, + real_cost, + test, ) -from .webapi import get_tasks, delete_old, download_log, download_json, load_simulation, real_cost -from .container import Job, Batch, BatchData from .cli import tidy3d_cli -from .cli.app import configure_fn as configure -from .asynchronous import run_async -from .webapi import test +from .api.asynchronous import run_async +from .core import core_config +from ..log import log, get_logging_console +from ..version import __version__ migrate() +core_config.set_config(log, get_logging_console(), __version__) + __all__ = [ "run", "upload", diff --git a/tidy3d/web/api/__init__.py b/tidy3d/web/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tidy3d/web/asynchronous.py b/tidy3d/web/api/asynchronous.py similarity index 76% rename from tidy3d/web/asynchronous.py rename to tidy3d/web/api/asynchronous.py index c127efce2..249037dce 100644 --- a/tidy3d/web/asynchronous.py +++ b/tidy3d/web/api/asynchronous.py @@ -2,12 +2,12 @@ from typing import Dict, List from .container import DEFAULT_DATA_DIR, BatchData, Batch -from ..components.simulation import Simulation -from ..log import log +from .tidy3d_stub import SimulationType +from ...log import log def run_async( - simulations: Dict[str, Simulation], + simulations: Dict[str, SimulationType], folder_name: str = "default", path_dir: str = DEFAULT_DATA_DIR, callback_url: str = None, @@ -16,12 +16,12 @@ def run_async( simulation_type: str = "tidy3d", parent_tasks: Dict[str, List[str]] = None, ) -> BatchData: - """Submits a set of :class:`.Simulation` objects to server, starts running, - monitors progress, downloads, and loads results as a :class:`.BatchData` object. + """Submits a set of Union[:class:`.Simulation`, :class:`.HeatSimulation`] objects to server, + starts running, monitors progress, downloads, and loads results as a :class:`.BatchData` object. Parameters ---------- - simulations : Dict[str, :class:`.Simulation`] + simulations : Dict[str, Union[:class:`.Simulation`, :class:`.HeatSimulation`]] Mapping of task name to simulation. folder_name : str = "default" Name of folder to store each task on web UI. @@ -38,7 +38,8 @@ def run_async( Returns ------ :class:`BatchData` - Contains the :class:`.SimulationData` of each :class:`.Simulation` in :class:`Batch`. + Contains the Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for each + Union[:class:`.Simulation`, :class:`.HeatSimulation`] in :class:`Batch`. """ if simulation_type is None: diff --git a/tidy3d/web/cacert.pem b/tidy3d/web/api/cacert.pem similarity index 100% rename from tidy3d/web/cacert.pem rename to tidy3d/web/api/cacert.pem diff --git a/tidy3d/web/api/connect_util.py b/tidy3d/web/api/connect_util.py new file mode 100644 index 000000000..1f0f4d585 --- /dev/null +++ b/tidy3d/web/api/connect_util.py @@ -0,0 +1,68 @@ +"""connect util for webapi.""" + +from functools import wraps +import time +from requests import ReadTimeout +from requests.exceptions import ConnectionError as ConnErr +from requests.exceptions import JSONDecodeError +from ...exceptions import WebError +from urllib3.exceptions import NewConnectionError +from ...log import log + +# number of seconds to keep re-trying connection before erroring +CONNECTION_RETRY_TIME = 180 +# time between checking task status +REFRESH_TIME = 0.3 + + +def wait_for_connection(decorated_fn=None, wait_time_sec: float = CONNECTION_RETRY_TIME): + """Causes function to ignore connection errors and retry for ``wait_time_sec`` secs.""" + + def decorator(web_fn): + """Decorator returned by @wait_for_connection()""" + + @wraps(web_fn) + def web_fn_wrapped(*args, **kwargs): + """Function to return including connection waiting.""" + time_start = time.time() + warned_previously = False + + while (time.time() - time_start) < wait_time_sec: + try: + return web_fn(*args, **kwargs) + except (ConnErr, ConnectionError, NewConnectionError, ReadTimeout, JSONDecodeError): + if not warned_previously: + log.warning(f"No connection: Retrying for {wait_time_sec} seconds.") + warned_previously = True + time.sleep(REFRESH_TIME) + + raise WebError("No internet connection: giving up on connection waiting.") + + return web_fn_wrapped + + if decorated_fn: + return decorator(decorated_fn) + + return decorator + + +def get_time_steps_str(time_steps) -> str: + """get_time_steps_str""" + if time_steps < 1000: + time_steps_str = f"{time_steps}" + elif 1000 <= time_steps < 1000 * 1000: + time_steps_str = f"{time_steps / 1000}K" + else: + time_steps_str = f"{time_steps / 1000 / 1000}M" + return time_steps_str + + +def get_grid_points_str(grid_points) -> str: + """get_grid_points_str""" + if grid_points < 1000: + grid_points_str = f"{grid_points}" + elif 1000 <= grid_points < 1000 * 1000: + grid_points_str = f"{grid_points / 1000}K" + else: + grid_points_str = f"{grid_points / 1000 / 1000}M" + return grid_points_str diff --git a/tidy3d/web/container.py b/tidy3d/web/api/container.py similarity index 80% rename from tidy3d/web/container.py rename to tidy3d/web/api/container.py index 6321c5601..58229d6ff 100644 --- a/tidy3d/web/container.py +++ b/tidy3d/web/api/container.py @@ -9,15 +9,14 @@ from rich.progress import Progress import pydantic.v1 as pd -from . import webapi as web -from .task import TaskId, TaskInfo, RunInfo, TaskName -from ..components.simulation import Simulation -from ..components.base import Tidy3dBaseModel -from ..components.data.sim_data import SimulationData -from ..log import log, get_logging_console - -from ..exceptions import DataError +from .tidy3d_stub import SimulationType, SimulationDataType +from ..api import webapi as web +from ..core.task_info import TaskInfo, RunInfo +from ..core.constants import TaskId, TaskName +from ...components.base import Tidy3dBaseModel +from ...log import log, get_logging_console +from ...exceptions import DataError DEFAULT_DATA_PATH = "simulation_data.hdf5" DEFAULT_DATA_DIR = "." @@ -28,10 +27,13 @@ class WebContainer(Tidy3dBaseModel, ABC): class Job(WebContainer): - """Interface for managing the running of a :class:`.Simulation` on server.""" + """Interface for managing the task runs on the server.""" - simulation: Simulation = pd.Field( - ..., title="Simulation", description="Simulation to run as a 'task'." + simulation: SimulationType = pd.Field( + ..., + title="simulation", + description="Simulation to run as a 'task'.", + discriminator="type", ) task_name: TaskName = pd.Field(..., title="Task Name", description="Unique name of the task.") @@ -85,8 +87,8 @@ class Job(WebContainer): "parent_tasks", ) - def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: - """run :class:`Job` all the way through and return data. + def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType: + """Run :class:`Job` all the way through and return data. Parameters ---------- @@ -95,8 +97,8 @@ def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: Returns ------- - Dict[str: :class:`.SimulationData`] - Dictionary mapping task name to :class:`.SimulationData` for :class:`Job`. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + Object containing simulation results. """ self.start() @@ -174,13 +176,12 @@ def download(self, path: str = DEFAULT_DATA_PATH) -> None: Note ---- - To load the data into :class:`.SimulationData`objets, can call :meth:`Job.load`. + To load the data after download, use :meth:`Job.load`. """ web.download(task_id=self.task_id, path=path, verbose=self.verbose) - def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: - """Download results from simulation (if not already) and load them into ``SimulationData`` - object. + def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType: + """Download job results and load them into a data object. Parameters ---------- @@ -189,8 +190,8 @@ def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: Returns ------- - :class:`.SimulationData` - Object containing data about simulation. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + Object containing simulation results. """ return web.load(task_id=self.task_id, path=path, verbose=self.verbose) @@ -198,33 +199,44 @@ def delete(self) -> None: """Delete server-side data associated with :class:`Job`.""" web.delete(self.task_id) - def real_cost(self) -> float: - """Get the billed cost for the task associated with this job.""" - return web.real_cost(self.task_id) + def real_cost(self, verbose: bool = True) -> float: + """Get the billed cost for the task associated with this job. - def estimate_cost(self) -> float: - """Compute the maximum FlexCredit charge for a given :class:`.Job`. + Parameters + ---------- + verbose : bool = True + Whether to log the cost and helpful messages. Returns ------- float - Estimated maximum cost for :class:`.Simulation` associated with given ``Job``. + Billed cost of the task in FlexCredits. + """ + return web.real_cost(self.task_id, verbose=verbose) - Note - ---- - Cost is calculated assuming the simulation runs for - the full ``run_time``. If early shut-off is triggered, the cost is adjusted proporionately. + def estimate_cost(self, verbose: bool = True) -> float: + """Compute the maximum FlexCredit charge for a given :class:`.Job`. + + Parameters + ---------- + verbose : bool = True + Whether to log the cost and helpful messages. Returns ------- float Estimated cost of the task in FlexCredits. + + Note + ---- + Cost is calculated assuming the simulation runs for + the full ``run_time``. If early shut-off is triggered, the cost is adjusted proportionately. """ - return web.estimate_cost(self.task_id) + return web.estimate_cost(self.task_id, verbose=verbose) class BatchData(Tidy3dBaseModel): - """Holds a collection of :class:`.SimulationData` returned by :class:`.Batch`.""" + """Holds a collection of data objects returned by :class:`.Batch`.""" task_paths: Dict[TaskName, str] = pd.Field( ..., @@ -240,8 +252,8 @@ class BatchData(Tidy3dBaseModel): True, title="Verbose", description="Whether to print info messages and progressbars." ) - def load_sim_data(self, task_name: str) -> SimulationData: - """Load a :class:`.SimulationData` from file by task name.""" + def load_sim_data(self, task_name: str) -> SimulationDataType: + """Load a simulation data object from file by task name.""" task_data_path = self.task_paths[task_name] task_id = self.task_ids[task_name] web.get_info(task_id) @@ -252,13 +264,13 @@ def load_sim_data(self, task_name: str) -> SimulationData: verbose=self.verbose, ) - def items(self) -> Tuple[TaskName, SimulationData]: - """Iterate through the :class:`.SimulationData` for each task_name.""" + def items(self) -> Tuple[TaskName, SimulationDataType]: + """Iterate through the simulations for each task_name.""" for task_name in self.task_paths.keys(): yield task_name, self.load_sim_data(task_name) - def __getitem__(self, task_name: TaskName) -> SimulationData: - """Get the :class:`.SimulationData` for a given ``task_name``.""" + def __getitem__(self, task_name: TaskName) -> SimulationDataType: + """Get the simulation data object for a given ``task_name``.""" return self.load_sim_data(task_name) @classmethod @@ -274,7 +286,8 @@ def load(cls, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: Returns ------ :class:`BatchData` - Contains the :class:`.SimulationData` of each :class:`.Simulation` in :class:`Batch`. + Contains Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + for each Union[:class:`.Simulation`, :class:`.HeatSimulation`] in :class:`Batch`. """ batch_file = Batch._batch_path(path_dir=path_dir) @@ -283,9 +296,9 @@ def load(cls, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: class Batch(WebContainer): - """Interface for submitting several :class:`.Simulation` objects to sever.""" + """Interface for submitting multiple simulations to the sever.""" - simulations: Dict[TaskName, Simulation] = pd.Field( + simulations: Dict[TaskName, SimulationType] = pd.Field( ..., title="Simulations", description="Mapping of task names to Simulations to run as a batch.", @@ -353,7 +366,8 @@ def run(self, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: Returns ------ :class:`BatchData` - Contains the :class:`.SimulationData` of each :class:`.Simulation` in :class:`Batch`. + Contains Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for + each Union[:class:`.Simulation`, :class:`.HeatSimulation`] in :class:`Batch`. Note ---- @@ -363,10 +377,9 @@ def run(self, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: >>> for task_name, sim_data in batch_data.items(): ... # do something with data. - ``bach_data`` does not store all of the :class:`.SimulationData` objects in memory, - rather it iterates over the task names - and loads the corresponding :class:`.SimulationData` from file one by one. - If no file exists for that task, it downloads it. + ``bach_data`` does not store all of the data objects in memory, + rather it iterates over the task names and loads the corresponding + data from file one by one. If no file exists for that task, it downloads it. """ self._check_path_dir(path_dir) self.start() @@ -389,6 +402,7 @@ def _upload(cls, val, values) -> None: parent_tasks = values.get("parent_tasks") verbose = bool(values.get("verbose")) + solver_version = values.get("solver_version") jobs = {} for task_name, simulation in values.get("simulations").items(): @@ -396,6 +410,7 @@ def _upload(cls, val, values) -> None: upload_kwargs["task_name"] = task_name upload_kwargs["simulation"] = simulation upload_kwargs["verbose"] = verbose + upload_kwargs["solver_version"] = solver_version if parent_tasks and task_name in parent_tasks: upload_kwargs["parent_tasks"] = parent_tasks[task_name] job = JobType(**upload_kwargs) @@ -474,13 +489,11 @@ def pbar_description(task_name: str, status: str) -> str: console = get_logging_console() console.log("Started working on Batch.") - est_flex_unit = self.estimate_cost() - if est_flex_unit is not None and est_flex_unit > 0: - console.log( - f"Maximum FlexCredit cost: {est_flex_unit:1.3f} for the whole batch. " - "Use 'Batch.real_cost()' to " - "get the billed FlexCredit cost after the Batch has completed." - ) + self.estimate_cost() + console.log( + "Use 'Batch.real_cost()' to " + "get the billed FlexCredit cost after the Batch has completed." + ) with Progress(console=console) as progress: # create progressbars @@ -570,7 +583,7 @@ def download(self, path_dir: str = DEFAULT_DATA_DIR) -> None: Note ---- - To load the data into :class:`.SimulationData`objets, can call :meth:`Batch.items`. + To load and iterate through the data, use :meth:`Batch.items()`. The data for each task will be named as ``{path_dir}/{task_name}.hdf5``. The :class:`Batch` hdf5 file will be automatically saved as ``{path_dir}/batch.hdf5``, @@ -599,7 +612,8 @@ def load(self, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: Returns ------ :class:`BatchData` - Contains the :class:`.SimulationData` of each :class:`.Simulation` in :class:`Batch`. + Contains Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for each + Union[:class:`.Simulation`, :class:`.HeatSimulation`] in :class:`Batch`. The :class:`Batch` hdf5 file will be automatically saved as ``{path_dir}/batch.hdf5``, allowing one to load this :class:`Batch` later using ``batch = Batch.from_file()``. @@ -627,31 +641,57 @@ def delete(self) -> None: for _, job in self.jobs.items(): job.delete() - def real_cost(self) -> float: - """Get the sum of billed costs for each task associated with this batch.""" + def real_cost(self, verbose: bool = True) -> float: + """Get the sum of billed costs for each task associated with this batch. + + Parameters + ---------- + verbose : bool = True + Whether to log the cost and helpful messages. + + Returns + ------- + float + Billed cost for the entire :class:`.Batch`. + """ real_cost_sum = 0.0 for _, job in self.jobs.items(): - cost_job = job.real_cost() + cost_job = job.real_cost(verbose=False) if cost_job is not None: real_cost_sum += cost_job - return real_cost_sum or None - def estimate_cost(self) -> float: + real_cost_sum = real_cost_sum or None # convert to None if 0 + + if real_cost_sum and verbose: + console = get_logging_console() + console.log(f"Total billed flex credit cost: {real_cost_sum:1.3f}.") + return real_cost_sum + + def estimate_cost(self, verbose: bool = True) -> float: """Compute the maximum FlexCredit charge for a given :class:`.Batch`. - Returns - ------- - float - Estimated maximum cost for each :class:`.Simulation` associated with given ``Batch``. + Parameters + ---------- + verbose : bool = True + Whether to log the cost and helpful messages. Note ---- Cost is calculated assuming the simulation runs for - the full ``run_time``. If early shut-off is triggered, the cost is adjusted proporionately. + the full ``run_time``. If early shut-off is triggered, the cost is adjusted proportionately. Returns ------- float Estimated total cost of the tasks in FlexCredits. """ - return sum(job.estimate_cost() for _, job in self.jobs.items()) + batch_cost = sum(job.estimate_cost(verbose=False) for _, job in self.jobs.items()) + + if verbose: + console = get_logging_console() + if batch_cost is not None and batch_cost > 0: + console.log(f"Maximum FlexCredit cost: {batch_cost:1.3f} for the whole batch.") + else: + console.log("Could not get estimated batch cost!") + + return batch_cost diff --git a/tidy3d/web/material_fitter.py b/tidy3d/web/api/material_fitter.py similarity index 96% rename from tidy3d/web/material_fitter.py rename to tidy3d/web/api/material_fitter.py index 81129710e..d9670543a 100644 --- a/tidy3d/web/material_fitter.py +++ b/tidy3d/web/api/material_fitter.py @@ -10,10 +10,10 @@ import numpy as np import requests from pydantic.v1 import BaseModel, Field -from tidy3d.plugins.dispersion import DispersionFitter +from ...plugins.dispersion import DispersionFitter -from .http_management import http -from .types import Submittable +from ..core.http_util import http +from ..core.types import Submittable class ConstraintEnum(str, Enum): diff --git a/tidy3d/web/material_libray.py b/tidy3d/web/api/material_libray.py similarity index 92% rename from tidy3d/web/material_libray.py rename to tidy3d/web/api/material_libray.py index dc8003d80..d36a87f27 100644 --- a/tidy3d/web/material_libray.py +++ b/tidy3d/web/api/material_libray.py @@ -6,10 +6,10 @@ from typing import List, Optional from pydantic.v1 import Field, parse_obj_as, validator -from tidy3d.components.medium import MediumType +from ...components.medium import MediumType -from .http_management import http -from .types import Queryable +from ..core.http_util import http +from ..core.types import Queryable class MaterialLibray(Queryable, smart_union=True): diff --git a/tidy3d/web/api/mode.py b/tidy3d/web/api/mode.py new file mode 100644 index 000000000..16f28c172 --- /dev/null +++ b/tidy3d/web/api/mode.py @@ -0,0 +1,477 @@ +"""Web API for mode solver""" + +from __future__ import annotations +from typing import Optional, Callable + +from datetime import datetime +import os +import pathlib +import tempfile +import time + +import pydantic.v1 as pydantic +from ...components.simulation import Simulation +from ...components.data.monitor_data import ModeSolverData +from ...exceptions import WebError +from ...log import log, get_logging_console +from ..core.http_util import http +from ..core.s3utils import download_file, upload_file +from ..core.task_core import Folder +from ..core.types import ResourceLifecycle, Submittable + +from ...plugins.mode.mode_solver import ModeSolver, MODE_MONITOR_NAME +from ...version import __version__ + +SIMULATION_JSON = "simulation.json" +SIM_FILE_HDF5_GZ = "simulation.hdf5.gz" +MODESOLVER_API = "tidy3d/modesolver/py" +MODESOLVER_JSON = "mode_solver.json" +MODESOLVER_HDF5 = "mode_solver.hdf5" +MODESOLVER_GZ = "mode_solver.hdf5.gz" + +MODESOLVER_LOG = "output/result.log" +MODESOLVER_RESULT = "output/result.hdf5" + + +def run( + mode_solver: ModeSolver, + task_name: str = "Untitled", + mode_solver_name: str = "mode_solver", + folder_name: str = "Mode Solver", + results_file: str = "mode_solver.hdf5", + verbose: bool = True, + progress_callback_upload: Callable[[float], None] = None, + progress_callback_download: Callable[[float], None] = None, +) -> ModeSolverData: + """Submits a :class:`.ModeSolver` to server, starts running, monitors progress, downloads, + and loads results as a :class:`.ModeSolverData` object. + + Parameters + ---------- + mode_solver : :class:`.ModeSolver` + Mode solver to upload to server. + task_name : str = "Untitled" + Name of task. + mode_solver_name: str = "mode_solver" + The name of the mode solver to create the in task. + folder_name : str = "Mode Solver" + Name of folder to store task on web UI. + results_file : str = "mode_solver.hdf5" + Path to download results file (.hdf5). + verbose : bool = True + If `True`, will print status, otherwise, will run silently. + progress_callback_upload : Callable[[float], None] = None + Optional callback function called when uploading file with ``bytes_in_chunk`` as argument. + progress_callback_download : Callable[[float], None] = None + Optional callback function called when downloading file with ``bytes_in_chunk`` as argument. + + Returns + ------- + :class:`.ModeSolverData` + Mode solver data with the calculated results. + """ + + log_level = "DEBUG" if verbose else "INFO" + if verbose: + console = get_logging_console() + + task = ModeSolverTask.create(mode_solver, task_name, mode_solver_name, folder_name) + if verbose: + console.log( + f"Mode solver created with task_id='{task.task_id}', solver_id='{task.solver_id}'." + ) + task.upload(verbose=verbose, progress_callback=progress_callback_upload) + task.submit() + + # Wait for task to finish + prev_status = "draft" + status = task.status + while status not in ("success", "error", "diverged", "deleted"): + if status != prev_status: + log.log(log_level, f"Mode solver status: {status}") + if verbose: + console.log(f"Mode solver status: {status}") + prev_status = status + time.sleep(0.5) + status = task.get_info().status + + if status == "error": + raise WebError("Error running mode solver.") + + log.log(log_level, f"Mode solver status: {status}") + if verbose: + console.log(f"Mode solver status: {status}") + + if status != "success": + # Our cache discards None, so the user is able to re-run + return None + + return task.get_result( + to_file=results_file, verbose=verbose, progress_callback=progress_callback_download + ) + + +class ModeSolverTask(ResourceLifecycle, Submittable, extra=pydantic.Extra.allow): + """Interface for managing the running of a :class:`.ModeSolver` task on server.""" + + task_id: str = pydantic.Field( + None, + title="task_id", + description="Task ID number, set when the task is created, leave as None.", + alias="refId", + ) + + solver_id: str = pydantic.Field( + None, + title="solver", + description="Solver ID number, set when the task is created, leave as None.", + alias="id", + ) + + real_flex_unit: float = pydantic.Field( + None, title="real FlexCredits", description="Billed FlexCredits.", alias="charge" + ) + + created_at: Optional[datetime] = pydantic.Field( + title="created_at", description="Time at which this task was created.", alias="createdAt" + ) + + status: str = pydantic.Field( + None, + title="status", + description="Mode solver task status.", + ) + + file_type: str = pydantic.Field( + None, + title="file_type", + description="File type used to upload the mode solver.", + alias="fileType", + ) + + mode_solver: ModeSolver = pydantic.Field( + None, + title="mode_solver", + description="Mode solver being run by this task.", + ) + + @classmethod + def create( + cls, + mode_solver: ModeSolver, + task_name: str = "Untitled", + mode_solver_name: str = "mode_solver", + folder_name: str = "Mode Solver", + ) -> ModeSolverTask: + """Create a new mode solver task on the server. + + Parameters + ---------- + mode_solver: :class".ModeSolver" + The object that will be uploaded to server in the submitting phase. + task_name: str = "Untitled" + The name of the task. + mode_solver_name: str = "mode_solver" + The name of the mode solver to create the in task. + folder_name: str = "Mode Solver" + The name of the folder to store the task. + + Returns + ------- + :class:`ModeSolverTask` + :class:`ModeSolverTask` object containing information about the created task. + """ + folder = Folder.get(folder_name, create=True) + + mode_solver.validate_pre_upload() + mode_solver.simulation.validate_pre_upload(source_required=False) + resp = http.post( + MODESOLVER_API, + { + "projectId": folder.folder_id, + "taskName": task_name, + "protocolVersion": __version__, + "modeSolverName": mode_solver_name, + "fileType": "Gz", + "source": "Python", + }, + ) + log.info( + "Mode solver created with task_id='%s', solver_id='%s'.", resp["refId"], resp["id"] + ) + return ModeSolverTask(**resp, mode_solver=mode_solver) + + @classmethod + def get( + cls, + task_id: str, + solver_id: str, + to_file: str = "mode_solver.hdf5", + sim_file: str = "simulation.hdf5", + verbose: bool = True, + progress_callback: Callable[[float], None] = None, + ) -> ModeSolverTask: + """Get mode solver task from the server by id. + + Parameters + ---------- + task_id: str + Unique identifier of the task on server. + solver_id: str + Unique identifier of the mode solver in the task. + to_file: str = "mode_solver.hdf5" + File to store the mode solver downloaded from the task. + sim_file: str = "simulation.hdf5" + File to store the simulation downloaded from the task. + verbose: bool = True + Whether to display progress bars. + progress_callback : Callable[[float], None] = None + Optional callback function called while downloading the data. + + Returns + ------- + :class:`ModeSolverTask` + :class:`ModeSolverTask` object containing information about the task. + """ + resp = http.get(f"{MODESOLVER_API}/{task_id}/{solver_id}") + task = ModeSolverTask(**resp) + mode_solver = task.get_modesolver(to_file, sim_file, verbose, progress_callback) + return task.copy(update={"mode_solver": mode_solver}) + + def get_info(self) -> ModeSolverTask: + """Get the current state of this task on the server. + + Returns + ------- + :class:`ModeSolverTask` + :class:`ModeSolverTask` object containing information about the task, without the mode + solver. + """ + resp = http.get(f"{MODESOLVER_API}/{self.task_id}/{self.solver_id}") + return ModeSolverTask(**resp, mode_solver=self.mode_solver) + + def upload( + self, verbose: bool = True, progress_callback: Callable[[float], None] = None + ) -> None: + """Upload this task's 'mode_solver' to the server. + + Parameters + ---------- + verbose: bool = True + Whether to display progress bars. + progress_callback : Callable[[float], None] = None + Optional callback function called while uploading the data. + """ + mode_solver = self.mode_solver.copy() + + sim = mode_solver.simulation + + # Upload simulation.hdf5.gz for GUI display + file, file_name = tempfile.mkstemp(".hdf5.gz") + os.close(file) + try: + sim.to_hdf5_gz(file_name) + upload_file( + self.task_id, + file_name, + SIM_FILE_HDF5_GZ, + verbose=verbose, + progress_callback=progress_callback, + ) + finally: + os.unlink(file_name) + + # Upload a single HDF5 file with the full data + file, file_name = tempfile.mkstemp(".hdf5.gz") + os.close(file) + try: + mode_solver.to_hdf5_gz(file_name) + upload_file( + self.solver_id, + file_name, + MODESOLVER_GZ, + verbose=verbose, + progress_callback=progress_callback, + ) + finally: + os.unlink(file_name) + + def submit(self): + """Start the execution of this task. + + The mode solver must be uploaded to the server with the :meth:`ModeSolverTask.upload` method + before this step. + """ + http.post(f"{MODESOLVER_API}/{self.task_id}/{self.solver_id}/run") + + def delete(self): + """Delete the mode solver and its corresponding task from the server.""" + # Delete mode solver + http.delete(f"{MODESOLVER_API}/{self.task_id}/{self.solver_id}") + # Delete parent task + http.delete(f"tidy3d/tasks/{self.task_id}") + + def abort(self): + """Abort the mode solver and its corresponding task from the server.""" + return http.put( + "tidy3d/tasks/abort", json={"taskType": "MODE_SOLVER", "taskId": self.solver_id} + ) + + def get_modesolver( + self, + to_file: str = "mode_solver.hdf5", + sim_file: str = "simulation.hdf5", + verbose: bool = True, + progress_callback: Callable[[float], None] = None, + ) -> ModeSolver: + """Get mode solver associated with this task from the server. + + Parameters + ---------- + to_file: str = "mode_solver.hdf5" + File to store the mode solver downloaded from the task. + sim_file: str = "simulation.hdf5" + File to store the simulation downloaded from the task, if any. + verbose: bool = True + Whether to display progress bars. + progress_callback : Callable[[float], None] = None + Optional callback function called while downloading the data. + + Returns + ------- + :class:`ModeSolver` + :class:`ModeSolver` object associated with this task. + """ + if self.file_type == "Gz": + file, file_path = tempfile.mkstemp(".hdf5.gz") + os.close(file) + try: + download_file( + self.solver_id, + MODESOLVER_GZ, + to_file=file_path, + verbose=verbose, + progress_callback=progress_callback, + ) + mode_solver = ModeSolver.from_hdf5_gz(file_path) + finally: + os.unlink(file_path) + + elif self.file_type == "Hdf5": + file, file_path = tempfile.mkstemp(".hdf5") + os.close(file) + try: + download_file( + self.solver_id, + MODESOLVER_HDF5, + to_file=file_path, + verbose=verbose, + progress_callback=progress_callback, + ) + mode_solver = ModeSolver.from_hdf5(file_path) + finally: + os.unlink(file_path) + + else: + file, file_path = tempfile.mkstemp(".json") + os.close(file) + try: + download_file( + self.solver_id, + MODESOLVER_JSON, + to_file=file_path, + verbose=verbose, + progress_callback=progress_callback, + ) + mode_solver_dict = ModeSolver.dict_from_json(file_path) + finally: + os.unlink(file_path) + + download_file( + self.task_id, + SIMULATION_JSON, + to_file=sim_file, + verbose=verbose, + progress_callback=progress_callback, + ) + mode_solver_dict["simulation"] = Simulation.from_json(sim_file) + mode_solver = ModeSolver.parse_obj(mode_solver_dict) + + # Store requested mode solver file + mode_solver.to_file(to_file) + + return mode_solver + + def get_result( + self, + to_file: str = "mode_solver_data.hdf5", + verbose: bool = True, + progress_callback: Callable[[float], None] = None, + ) -> ModeSolverData: + """Get mode solver results for this task from the server. + + Parameters + ---------- + to_file: str = "mode_solver_data.hdf5" + File to store the mode solver downloaded from the task. + verbose: bool = True + Whether to display progress bars. + progress_callback : Callable[[float], None] = None + Optional callback function called while downloading the data. + + Returns + ------- + :class:`.ModeSolverData` + Mode solver data with the calculated results. + """ + try: + download_file( + self.solver_id, + MODESOLVER_RESULT, + to_file=to_file, + verbose=verbose, + progress_callback=progress_callback, + ) + except Exception: + raise WebError( + f"Failed to download file '{MODESOLVER_RESULT}' from server. Please confirm that the task was successful." + ) + data = ModeSolverData.from_hdf5(to_file) + data = data.copy( + update={"monitor": self.mode_solver.to_mode_solver_monitor(name=MODE_MONITOR_NAME)} + ) + + self.mode_solver._cached_properties["data_raw"] = data + + # Perform symmetry expansion + return self.mode_solver.data + + def get_log( + self, + to_file: str = "mode_solver.log", + verbose: bool = True, + progress_callback: Callable[[float], None] = None, + ) -> pathlib.Path: + """Get execution log for this task from the server. + + Parameters + ---------- + to_file: str = "mode_solver.log" + File to store the mode solver downloaded from the task. + verbose: bool = True + Whether to display progress bars. + progress_callback : Callable[[float], None] = None + Optional callback function called while downloading the data. + + Returns + ------- + path: pathlib.Path + Path to saved file. + """ + return download_file( + self.solver_id, + MODESOLVER_LOG, + to_file=to_file, + verbose=verbose, + progress_callback=progress_callback, + ) diff --git a/tidy3d/web/api/tidy3d_stub.py b/tidy3d/web/api/tidy3d_stub.py new file mode 100644 index 000000000..54d6922e8 --- /dev/null +++ b/tidy3d/web/api/tidy3d_stub.py @@ -0,0 +1,214 @@ +"""Stub for webapi""" +from __future__ import annotations + +import json +from typing import Union, Callable, List + +import pydantic.v1 as pd +from pydantic.v1 import BaseModel + +from ..core.file_util import ( + read_simulation_from_json, + read_simulation_from_hdf5, + read_simulation_from_hdf5_gz, +) +from ..core.stub import TaskStub, TaskStubData +from ... import log +from ...components.base import _get_valid_extension +from ...components.data.sim_data import SimulationData +from ...components.data.monitor_data import ModeSolverData +from ..core.types import TaskType +from ...components.simulation import Simulation +from ...plugins.mode.mode_solver import ModeSolver +from ...components.heat.simulation import HeatSimulation +from ...components.heat.data.sim_data import HeatSimulationData + +SimulationType = Union[Simulation, HeatSimulation] +SimulationDataType = Union[SimulationData, HeatSimulationData] + + +class Tidy3dStub(BaseModel, TaskStub): + + simulation: SimulationType = pd.Field(discriminator="type") + + @classmethod + def from_file(cls, file_path: str) -> SimulationType: + """Loads a Union[:class:`.Simulation`, :class:`.HeatSimulation`] + from .yaml, .json, or .hdf5 file. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to load the + Union[:class:`.Simulation`, :class:`.HeatSimulation`] from. + + Returns + ------- + Union[:class:`.Simulation`, :class:`.HeatSimulation`] + An instance of the component class calling `load`. + + Example + ------- + >>> simulation = Simulation.from_file(fname='folder/sim.json') # doctest: +SKIP + """ + extension = _get_valid_extension(file_path) + if extension == ".json": + json_str = read_simulation_from_json(file_path) + elif extension == ".hdf5": + json_str = read_simulation_from_hdf5(file_path) + elif extension == ".hdf5.gz": + json_str = read_simulation_from_hdf5_gz(file_path) + + data = json.loads(json_str) + type_ = data["type"] + if "Simulation" == type_: + sim = Simulation.from_file(file_path) + elif "ModeSolver" == type_: + sim = ModeSolver.from_file(file_path) + elif "HeatSimulation" == type_: + sim = HeatSimulation.from_file(file_path) + + return sim + + def to_file( + self, + file_path: str, + ): + """Exports Union[:class:`.Simulation`, :class:`.HeatSimulation`] instance to .yaml, .json, + or .hdf5 file + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to save the :class:`Stub` to. + + Example + ------- + >>> simulation.to_file(fname='folder/sim.json') # doctest: +SKIP + """ + self.simulation.to_file(file_path) + + def to_hdf5_gz(self, fname: str, custom_encoders: List[Callable] = None) -> None: + """Exports Union[:class:`.Simulation`, :class:`.HeatSimulation`] instance to .hdf5.gz file. + + Parameters + ---------- + fname : str + Full path to the .hdf5.gz file to save + the Union[:class:`.Simulation`, :class:`.HeatSimulation`] to. + custom_encoders : List[Callable] + List of functions accepting (fname: str, group_path: str, value: Any) that take + the ``value`` supplied and write it to the hdf5 ``fname`` at ``group_path``. + + Example + ------- + >>> simulation.to_hdf5_gz(fname='folder/sim.hdf5.gz') # doctest: +SKIP + """ + + self.simulation.to_hdf5_gz(fname) + + def get_type(self) -> str: + """Get simulation instance type. + + Returns + ------- + :class:`TaskType` + An instance Type of the component class calling `load`. + """ + if isinstance(self.simulation, Simulation): + return TaskType.FDTD.name + elif isinstance(self.simulation, ModeSolver): + return TaskType.MODE_SOLVER.name + elif isinstance(self.simulation, HeatSimulation): + return TaskType.HEAT.name + + def validate_pre_upload(self, source_required) -> None: + """Perform some pre-checks on instances of component""" + if isinstance(self.simulation, Simulation): + self.simulation.validate_pre_upload(source_required) + + +class Tidy3dStubData(BaseModel, TaskStubData): + """""" + + data: SimulationDataType + + @classmethod + def from_file(cls, file_path: str) -> SimulationDataType: + """Loads a Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + from .yaml, .json, or .hdf5 file. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to load the + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] from. + + Returns + ------- + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + An instance of the component class calling `load`. + """ + extension = _get_valid_extension(file_path) + if extension == ".json": + json_str = read_simulation_from_json(file_path) + elif extension == ".hdf5": + json_str = read_simulation_from_hdf5(file_path) + elif extension == ".hdf5.gz": + json_str = read_simulation_from_hdf5_gz(file_path) + + data = json.loads(json_str) + type_ = data["type"] + if "SimulationData" == type_: + sim_data = SimulationData.from_file(file_path) + elif "ModeSolverData" == type_: + sim_data = ModeSolverData.from_file(file_path) + elif "HeatSimulationData" == type_: + sim_data = HeatSimulationData.from_file(file_path) + + return sim_data + + def to_file(self, file_path: str): + """Exports Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] instance + to .yaml, .json, or .hdf5 file + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to save the + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] to. + + Example + ------- + >>> simulation.to_file(fname='folder/sim.json') # doctest: +SKIP + """ + self.data.to_file(file_path) + + @classmethod + def postprocess(cls, file_path: str) -> SimulationDataType: + """Load .yaml, .json, or .hdf5 file to + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] instance. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to save the + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] to. + + Returns + ------- + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + An instance of the component class calling `load`. + """ + stub_data = Tidy3dStubData.from_file(file_path) + + if isinstance(stub_data, SimulationData): + final_decay_value = stub_data.final_decay_value + shutoff_value = stub_data.simulation.shutoff + if (shutoff_value != 0) and (final_decay_value > shutoff_value): + log.warning( + f"Simulation final field decay value of {final_decay_value} is greater than " + f"the simulation shutoff threshold of {shutoff_value}. Consider running the " + "simulation again with a larger 'run_time' duration for more accurate results." + ) + return stub_data diff --git a/tidy3d/web/webapi.py b/tidy3d/web/api/webapi.py similarity index 63% rename from tidy3d/web/webapi.py rename to tidy3d/web/api/webapi.py index 014f672f0..9b7a796ce 100644 --- a/tidy3d/web/webapi.py +++ b/tidy3d/web/api/webapi.py @@ -4,27 +4,24 @@ import time from datetime import datetime, timedelta from typing import List, Dict, Callable -from functools import wraps - -from requests import HTTPError, ReadTimeout -from requests.exceptions import ConnectionError as ConnErr -from requests.exceptions import JSONDecodeError -from urllib3.exceptions import NewConnectionError - +from requests import HTTPError import pytz from rich.progress import Progress -from .environment import Env -from .simulation_task import SimulationTask, SIM_FILE_HDF5, Folder -from .task import TaskId, TaskInfo, ChargeType -from ..components.data.sim_data import SimulationData -from ..components.simulation import Simulation -from ..components.types import Literal -from ..log import log, get_logging_console -from ..exceptions import WebError - -# time between checking task status -REFRESH_TIME = 0.3 +from .tidy3d_stub import Tidy3dStub, Tidy3dStubData, SimulationType, SimulationDataType +from .connect_util import ( + wait_for_connection, + REFRESH_TIME, + get_time_steps_str, + get_grid_points_str, +) +from ..core.environment import Env +from ..core.constants import SIM_FILE_HDF5, TaskId +from ..core.task_core import SimulationTask, Folder +from ..core.task_info import TaskInfo, ChargeType +from ...components.types import Literal +from ...log import log, get_logging_console +from ...exceptions import WebError # time between checking run status RUN_REFRESH_TIME = 1.0 @@ -32,40 +29,6 @@ # file names when uploading to S3 SIM_FILE_JSON = "simulation.json" -# number of seconds to keep re-trying connection before erroring -CONNECTION_RETRY_TIME = 180 - - -def wait_for_connection(decorated_fn=None, wait_time_sec: float = CONNECTION_RETRY_TIME): - """Causes function to ignore connection errors and retry for ``wait_time_sec`` secs.""" - - def decorator(web_fn): - """Decorator returned by @wait_for_connection()""" - - @wraps(web_fn) - def web_fn_wrapped(*args, **kwargs): - """Function to return including connection waiting.""" - time_start = time.time() - warned_previously = False - - while (time.time() - time_start) < wait_time_sec: - try: - return web_fn(*args, **kwargs) - except (ConnErr, ConnectionError, NewConnectionError, ReadTimeout, JSONDecodeError): - if not warned_previously: - log.warning(f"No connection: Retrying for {wait_time_sec} seconds.") - warned_previously = True - time.sleep(REFRESH_TIME) - - raise WebError("No internet connection: giving up on connection waiting.") - - return web_fn_wrapped - - if decorated_fn: - return decorator(decorated_fn) - - return decorator - def _get_url(task_id: str) -> str: """Get the URL for a task on our server.""" @@ -74,7 +37,7 @@ def _get_url(task_id: str) -> str: @wait_for_connection def run( - simulation: Simulation, + simulation: SimulationType, task_name: str, folder_name: str = "default", path: str = "simulation_data.hdf5", @@ -84,13 +47,13 @@ def run( progress_callback_download: Callable[[float], None] = None, solver_version: str = None, worker_group: str = None, -) -> SimulationData: - """Submits a :class:`.Simulation` to server, starts running, monitors progress, downloads, - and loads results as a :class:`.SimulationData` object. +) -> SimulationDataType: + """Submits a simulation to the server, starts running, monitors progress, + downloads, and loads results as a corresponding data object. Parameters ---------- - simulation : :class:`.Simulation` + simulation : Union[:class:`.Simulation`, :class:`.HeatSimulation`] Simulation to upload to server. task_name : str Name of task. @@ -114,8 +77,8 @@ def run( Returns ------- - :class:`.SimulationData` - Object containing solver results for the supplied :class:`.Simulation`. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + Object containing solver results for the supplied simulation. """ task_id = upload( simulation=simulation, @@ -138,7 +101,7 @@ def run( @wait_for_connection def upload( - simulation: Simulation, + simulation: SimulationType, task_name: str, folder_name: str = "default", callback_url: str = None, @@ -148,11 +111,11 @@ def upload( parent_tasks: List[str] = None, source_required: bool = True, ) -> TaskId: - """Upload simulation to server, but do not start running :class:`.Simulation`. + """Upload a simulation to the server, but do not start running. Parameters ---------- - simulation : :class:`.Simulation` + simulation : Union[:class:`.Simulation`, :class:`.HeatSimulation`] Simulation to upload to server. task_name : str Name of task. @@ -181,45 +144,55 @@ def upload( ---- To start the simulation running, must call :meth:`start` after uploaded. """ - - simulation.validate_pre_upload(source_required=source_required) + stub = Tidy3dStub(simulation=simulation) + stub.validate_pre_upload(source_required=source_required) log.debug("Creating task.") + task_type = stub.get_type() + task = SimulationTask.create( - simulation, task_name, folder_name, callback_url, simulation_type, parent_tasks, "Gz" + task_type, task_name, folder_name, callback_url, simulation_type, parent_tasks, "Gz" ) if verbose: console = get_logging_console() - console.log(f"Created task '{task_name}' with task_id '{task.task_id}'.") - url = _get_url(task.task_id) - console.log(f"View task using web UI at [blue underline][link={url}]'{url}'[/link].") - task.upload_simulation(verbose=verbose, progress_callback=progress_callback) + console.log( + f"Created task '{task_name}' with task_id '{task.task_id}' and task_type '{task_type}'." + ) + if task_type == "HEAT": + console.log( + "Tidy3D's heat solver is currently in the beta stage. All heat simulations are " + "charged a flat fee of 0.025 FlexCredit." + ) + else: + url = _get_url(task.task_id) + console.log(f"View task using web UI at [link={url}]'{url}'[/link].") + + task.upload_simulation(stub=stub, verbose=verbose, progress_callback=progress_callback) # log the url for the task in the web UI - log.debug( - f"{Env.current.website_endpoint}/folders/{task.folder.folder_id}/tasks/{task.task_id}" - ) + log.debug(f"{Env.current.website_endpoint}/folders/{task.folder_id}/tasks/{task.task_id}") return task.task_id @wait_for_connection -def get_info(task_id: TaskId) -> TaskInfo: +def get_info(task_id: TaskId, verbose: bool = True) -> TaskInfo: """Return information about a task. Parameters ---------- task_id : str Unique identifier of task on server. Returned by :meth:`upload`. - + verbose : bool = True + If `True`, will print progressbars and status, otherwise, will run silently. Returns ------- :class:`TaskInfo` Object containing information about status, size, credits of task. """ - task = SimulationTask.get(task_id) + task = SimulationTask.get(task_id, verbose) if not task: raise ValueError("Task not found.") - return TaskInfo(**{"taskId": task.task_id, **task.dict()}) + return TaskInfo(**{"taskId": task.task_id, "taskType": task.task_type, **task.dict()}) @wait_for_connection @@ -247,7 +220,10 @@ def start( task = SimulationTask.get(task_id) if not task: raise ValueError("Task not found.") - task.submit(solver_version=solver_version, worker_group=worker_group) + task.submit( + solver_version=solver_version, + worker_group=worker_group, + ) @wait_for_connection @@ -265,7 +241,7 @@ def get_run_info(task_id: TaskId): Percentage of run done (in terms of max number of time steps). Is ``None`` if run info not available. field_decay : float - Average field intensity normlized to max value (1.0). + Average field intensity normalized to max value (1.0). Is ``None`` if run info not available. """ task = SimulationTask(taskId=task_id) @@ -290,7 +266,6 @@ def get_status(task_id) -> str: def monitor(task_id: TaskId, verbose: bool = True) -> None: - """Print the real time task progress until completion. Parameters @@ -305,137 +280,158 @@ def monitor(task_id: TaskId, verbose: bool = True) -> None: To load results when finished, may call :meth:`load`. """ - task_info = get_info(task_id) - task_name = task_info.taskName - - break_statuses = ("success", "error", "diverged", "deleted", "draft", "abort") - console = get_logging_console() if verbose else None - def get_estimated_cost() -> float: - """Get estimated cost, if None, is not ready.""" - task_info = get_info(task_id) - block_info = task_info.taskBlockInfo - if block_info and block_info.chargeType == ChargeType.FREE: - est_flex_unit = 0 - grid_points = block_info.maxGridPoints - time_steps = block_info.maxTimeSteps - if grid_points < 1000: - grid_points_str = f"{grid_points}" - elif 1000 <= grid_points < 1000 * 1000: - grid_points_str = f"{grid_points / 1000}K" - else: - grid_points_str = f"{grid_points / 1000 / 1000}M" + task_info = get_info(task_id) - if time_steps < 1000: - time_steps_str = f"{time_steps}" - elif 1000 <= time_steps < 1000 * 1000: - time_steps_str = f"{time_steps / 1000}K" - else: - time_steps_str = f"{time_steps / 1000 / 1000}M" + if task_info.taskType in ("MODE_SOLVER", "HEAT"): - console.log( - f"You are running this simulation for FREE. Your current plan allows" - f" up to {block_info.maxFreeCount} free non-concurrent simulations per" - f" day (under {grid_points_str} grid points and {time_steps_str}" - f" time steps)" - ) - else: - est_flex_unit = task_info.estFlexUnit - if est_flex_unit is not None and est_flex_unit > 0: + log_level = "DEBUG" if verbose else "INFO" + solver_name = "Mode" if task_info.taskType == "MODE_SOLVER" else "Heat" + + # Wait for task to finish + prev_status = "draft" + status = get_status(task_id) + while status not in ("success", "error", "diverged", "deleted"): + if status != prev_status: + log.log(log_level, f"{solver_name} solver status: {status}") + if verbose: + console.log(f"{solver_name} solver status: {status}") + prev_status = status + time.sleep(0.5) + status = get_status(task_id) + + if status == "error": + raise WebError(f"Error running {solver_name} solver.") + + log.log(log_level, f"{solver_name} solver status: {status}") + if verbose: + console.log(f"{solver_name} solver status: {status}") + + if status != "success": + # Our cache discards None, so the user is able to re-run + return None + + elif task_info.taskType == "FDTD": + + task_name = task_info.taskName + + break_statuses = ("success", "error", "diverged", "deleted", "draft", "abort") + + def get_estimated_cost() -> float: + """Get estimated cost, if None, is not ready.""" + task_info = get_info(task_id) + block_info = task_info.taskBlockInfo + if block_info and block_info.chargeType == ChargeType.FREE: + est_flex_unit = 0 + grid_points = block_info.maxGridPoints + time_steps = block_info.maxTimeSteps + grid_points_str = get_grid_points_str(grid_points) + time_steps_str = get_time_steps_str(time_steps) console.log( - f"Maximum FlexCredit cost: {est_flex_unit:1.3f}. Use 'web.real_cost(task_id)'" - f" to get the billed FlexCredit cost after a simulation run." + f"You are running this simulation for FREE. Your current plan allows" + f" up to {block_info.maxFreeCount} free non-concurrent simulations per" + f" day (under {grid_points_str} grid points and {time_steps_str}" + f" time steps)" ) - return est_flex_unit + else: + est_flex_unit = task_info.estFlexUnit + if est_flex_unit is not None and est_flex_unit > 0: + console.log( + f"Maximum FlexCredit cost: {est_flex_unit:1.3f}. Use 'web.real_cost(task_id)'" + f" to get the billed FlexCredit cost after a simulation run." + ) + return est_flex_unit + + def monitor_preprocess() -> None: + """Periodically check the status.""" + status = get_status(task_id) + while status not in break_statuses and status != "running": + new_status = get_status(task_id) + if new_status != status: + status = new_status + if verbose and status != "running": + console.log(f"status = {status}") + time.sleep(REFRESH_TIME) - def monitor_preprocess() -> None: - """Periodically check the status.""" status = get_status(task_id) - while status not in break_statuses and status != "running": - new_status = get_status(task_id) - if new_status != status: - status = new_status - if verbose and status != "running": - console.log(f"status = {status}") - time.sleep(REFRESH_TIME) - status = get_status(task_id) - - if verbose: - console.log(f"status = {status}") + if verbose: + console.log(f"status = {status}") - # already done - if status in break_statuses: - return + # already done + if status in break_statuses: + return - # preprocessing - if verbose: - with console.status(f"[bold green]Starting '{task_name}'...", spinner="runner"): + # preprocessing + if verbose: + with console.status(f"[bold green]Starting '{task_name}'...", spinner="runner"): + monitor_preprocess() + else: monitor_preprocess() - else: - monitor_preprocess() - # if the estimated cost is ready, print it - if verbose: - get_estimated_cost() - console.log("starting up solver") + # if the estimated cost is ready, print it + if verbose: + get_estimated_cost() + console.log("starting up solver") - # while running but before the percentage done is available, keep waiting - while get_run_info(task_id)[0] is None and get_status(task_id) == "running": - time.sleep(REFRESH_TIME) + # while running but before the percentage done is available, keep waiting + while get_run_info(task_id)[0] is None and get_status(task_id) == "running": + time.sleep(REFRESH_TIME) - # while running but percentage done is available - if verbose: - # verbose case, update progressbar - console.log("running solver") - console.log( - "To cancel the simulation, use 'web.abort(task_id)' or 'web.delete(task_id)' " - "or abort/delete the task in the web " - "UI. Terminating the Python script will not stop the job running on the cloud." - ) - with Progress(console=console) as progress: - pbar_pd = progress.add_task("% done", total=100) - perc_done, _ = get_run_info(task_id) + # while running but percentage done is available + if verbose: + # verbose case, update progressbar + console.log("running solver") + console.log( + "To cancel the simulation, use 'web.abort(task_id)' or 'web.delete(task_id)' " + "or abort/delete the task in the web " + "UI. Terminating the Python script will not stop the job running on the cloud." + ) + with Progress(console=console) as progress: + pbar_pd = progress.add_task("% done", total=100) + perc_done, _ = get_run_info(task_id) + + while ( + perc_done is not None and perc_done < 100 and get_status(task_id) == "running" + ): + perc_done, field_decay = get_run_info(task_id) + new_description = f"solver progress (field decay = {field_decay:.2e})" + progress.update(pbar_pd, completed=perc_done, description=new_description) + time.sleep(RUN_REFRESH_TIME) - while perc_done is not None and perc_done < 100 and get_status(task_id) == "running": perc_done, field_decay = get_run_info(task_id) - new_description = f"solver progress (field decay = {field_decay:.2e})" - progress.update(pbar_pd, completed=perc_done, description=new_description) - time.sleep(RUN_REFRESH_TIME) - - perc_done, field_decay = get_run_info(task_id) - if perc_done is not None and perc_done < 100 and field_decay > 0: - console.log("early shutoff detected, exiting.") - - new_description = f"solver progress (field decay = {field_decay:.2e})" - progress.update(pbar_pd, completed=100, refresh=True, description=new_description) + if perc_done is not None and perc_done < 100 and field_decay > 0: + console.log(f"early shutoff detected at {perc_done:1.0f}%, exiting.") - else: - # non-verbose case, just keep checking until status is not running or perc_done >= 100 - perc_done, _ = get_run_info(task_id) - while perc_done is not None and perc_done < 100 and get_status(task_id) == "running": - perc_done, field_decay = get_run_info(task_id) - time.sleep(1.0) - - # post processing - if verbose: - status = get_status(task_id) - if status != "running": - console.log(f"status = {status}") + new_description = f"solver progress (field decay = {field_decay:.2e})" + progress.update(pbar_pd, completed=100, refresh=True, description=new_description) - with console.status(f"[bold green]Finishing '{task_name}'...", spinner="runner"): - while status not in break_statuses: - new_status = get_status(task_id) - if new_status != status: - status = new_status - console.log(f"status = {status}") + else: + # non-verbose case, just keep checking until status is not running or perc_done >= 100 + perc_done, _ = get_run_info(task_id) + while perc_done is not None and perc_done < 100 and get_status(task_id) == "running": + perc_done, field_decay = get_run_info(task_id) + time.sleep(1.0) + + # post processing + if verbose: + status = get_status(task_id) + if status != "running": + console.log(f"status = {status}") + + with console.status(f"[bold green]Finishing '{task_name}'...", spinner="runner"): + while status not in break_statuses: + new_status = get_status(task_id) + if new_status != status: + status = new_status + console.log(f"status = {status}") + time.sleep(REFRESH_TIME) + url = _get_url(task_id) + console.log(f"View simulation result at [blue underline][link={url}]'{url}'[/link].") + else: + while get_status(task_id) not in break_statuses: time.sleep(REFRESH_TIME) - url = _get_url(task_id) - console.log(f"View simulation result at [blue underline][link={url}]'{url}'[/link].") - else: - while get_status(task_id) not in break_statuses: - time.sleep(REFRESH_TIME) @wait_for_connection @@ -508,8 +504,10 @@ def download_hdf5( @wait_for_connection -def load_simulation(task_id: TaskId, path: str = SIM_FILE_JSON, verbose: bool = True) -> Simulation: - """Download the `.json` file of a task and load the associated :class:`.Simulation`. +def load_simulation( + task_id: TaskId, path: str = SIM_FILE_JSON, verbose: bool = True +) -> SimulationType: + """Download the `.json` file of a task and load the associated simulation. Parameters ---------- @@ -522,15 +520,13 @@ def load_simulation(task_id: TaskId, path: str = SIM_FILE_JSON, verbose: bool = Returns ------- - :class:`.Simulation` + Union[:class:`.Simulation`, :class:`.HeatSimulation`] Simulation loaded from downloaded json file. """ - # task = SimulationTask.get(task_id) - - task = SimulationTask(taskId=task_id) + task = SimulationTask.get(task_id) task.get_simulation_json(path, verbose=verbose) - return Simulation.from_file(path) + return Tidy3dStub.from_file(path) @wait_for_connection @@ -568,8 +564,8 @@ def load( replace_existing: bool = True, verbose: bool = True, progress_callback: Callable[[float], None] = None, -) -> SimulationData: - """Download and Load simulation results into :class:`.SimulationData` object. +) -> SimulationDataType: + """Download and load simulation results into a data object. Parameters ---------- @@ -586,29 +582,18 @@ def load( Returns ------- - :class:`.SimulationData` + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] Object containing simulation data. """ - if not os.path.exists(path) or replace_existing: download(task_id=task_id, path=path, verbose=verbose, progress_callback=progress_callback) if verbose: console = get_logging_console() - console.log(f"loading SimulationData from {path}") - - sim_data = SimulationData.from_file(path) - - final_decay_value = sim_data.final_decay_value - shutoff_value = sim_data.simulation.shutoff - if (shutoff_value != 0) and (final_decay_value > shutoff_value): - log.warning( - f"Simulation final field decay value of {final_decay_value} " - f"is greater than the simulation shutoff threshold of {shutoff_value}. " - "Consider simulation again with large run_time duration for more accurate results." - ) + console.log(f"loading simulation from {path}") - return sim_data + stub_data = Tidy3dStubData.postprocess(path) + return stub_data @wait_for_connection @@ -667,7 +652,7 @@ def delete_old( @wait_for_connection -def abort(task_id: TaskId) -> TaskInfo: +def abort(task_id: TaskId): """Abort server-side data associated with task. Parameters @@ -681,13 +666,12 @@ def abort(task_id: TaskId) -> TaskInfo: Object containing information about status, size, credits of task. """ - # task = SimulationTask.get(task_id) - task = SimulationTask(taskId=task_id) + task = SimulationTask.get(task_id) + # task = SimulationTask(taskId=task_id) task.abort() return TaskInfo(**{"taskId": task.task_id, **task.dict()}) -# TODO: make this return a list of TaskInfo instead? @wait_for_connection def get_tasks( num_tasks: int = None, order: Literal["new", "old"] = "new", folder: str = "default" @@ -722,23 +706,26 @@ def get_tasks( @wait_for_connection -def estimate_cost(task_id: str) -> float: +def estimate_cost(task_id: str, verbose: bool = True) -> float: """Compute the maximum FlexCredit charge for a given task. Parameters ---------- task_id : str Unique identifier of task on server. Returned by :meth:`upload`. + verbose : bool = True + Whether to log the cost and helpful messages. Returns ------- float - Estimated maximum cost for :class:`.Simulation` associated with given ``task_id``.. + Estimated maximum cost for :class:`.Simulation` associated with given ``task_id``. Note ---- Cost is calculated assuming the simulation runs for the full ``run_time``. If early shut-off is triggered, the cost is adjusted proportionately. + A minimum simulation cost may also apply, which depends on the task details. Parameters ---------- @@ -766,6 +753,19 @@ def estimate_cost(task_id: str) -> float: status = task_info.metadataStatus if status in ["processed", "success"]: + if verbose: + console = get_logging_console() + console.log( + f"Maximum FlexCredit cost: {task_info.estFlexUnit:1.3f}. Minimum cost depends on " + "task execution details. Use 'web.real_cost(task_id)' to get the billed FlexCredit " + "cost after a simulation run." + ) + fc_mode = task_info.estFlexCreditMode + fc_post = task_info.estFlexCreditPostProcess + if fc_mode: + console.log(f" {fc_mode:1.3f} FlexCredit of the total cost from mode solves.") + if fc_post: + console.log(f" {fc_post:1.3f} FlexCredit of the total cost from post-processing.") return task_info.estFlexUnit log.warning( @@ -776,9 +776,21 @@ def estimate_cost(task_id: str) -> float: @wait_for_connection -def real_cost(task_id: str) -> float: +def real_cost(task_id: str, verbose=True) -> float: """Get the billed cost for given task after it has been run. + Parameters + ---------- + task_id : str + Unique identifier of task on server. Returned by :meth:`upload`. + verbose : bool = True + Whether to log the cost and helpful messages. + + Returns + ------- + float + The flex credit cost that was billed for the given ``task_id``. + Note ---- The billed cost may not be immediately available when the task status is set to ``success``, @@ -786,11 +798,22 @@ def real_cost(task_id: str) -> float: """ task_info = get_info(task_id) flex_unit = task_info.realFlexUnit + ori_flex_unit = task_info.oriRealFlexUnit if not flex_unit: log.warning( f"Billed FlexCredit for task '{task_id}' is not available. If the task has been " "successfully run, it should be available shortly." ) + else: + if verbose: + console = get_logging_console() + console.log(f"Billed flex credit cost: {flex_unit:1.3f}.") + if flex_unit != ori_flex_unit: + console.log( + "Note: the task cost pro-rated due to early shutoff was below the minimum " + "threshold, due to fast shutoff. Decreasing the simulation 'run_time' should " + "decrease the estimated, and correspondingly the billed cost of such tasks." + ) return flex_unit diff --git a/tidy3d/web/cli/app.py b/tidy3d/web/cli/app.py index 9333c617c..7f0387ee8 100644 --- a/tidy3d/web/cli/app.py +++ b/tidy3d/web/cli/app.py @@ -9,10 +9,11 @@ import requests import toml -from tidy3d.web.cli.constants import TIDY3D_DIR, CONFIG_FILE, CREDENTIAL_FILE -from tidy3d.web.cli.migrate import migrate -from tidy3d.web.environment import Env -from tidy3d.web.cli.converter import converter_arg +from ..cli.constants import TIDY3D_DIR, CONFIG_FILE, CREDENTIAL_FILE +from ..cli.migrate import migrate +from ..core.constants import KEY_APIKEY, HEADER_APIKEY +from ..core.environment import Env +from ..cli.converter import converter_arg if not os.path.exists(TIDY3D_DIR): os.mkdir(TIDY3D_DIR) @@ -30,7 +31,7 @@ def get_description(): with open(CONFIG_FILE, encoding="utf-8") as f: content = f.read() config = toml.loads(content) - return config.get("apikey", "") + return config.get(KEY_APIKEY, "") return "" @@ -74,7 +75,7 @@ def auth(req): requests.Request Enriched request. """ - req.headers["simcloud-api-key"] = apikey + req.headers[HEADER_APIKEY] = apikey return req if os.path.exists(CREDENTIAL_FILE): @@ -103,7 +104,7 @@ def auth(req): click.echo("Configured successfully.") with open(CONFIG_FILE, "w+", encoding="utf-8") as config_file: toml_config = toml.loads(config_file.read()) - toml_config.update({"apikey": apikey}) + toml_config.update({KEY_APIKEY: apikey}) config_file.write(toml.dumps(toml_config)) else: click.echo("API key is invalid.") diff --git a/tidy3d/web/cli/migrate.py b/tidy3d/web/cli/migrate.py index 800e30bd3..9b31c071a 100644 --- a/tidy3d/web/cli/migrate.py +++ b/tidy3d/web/cli/migrate.py @@ -7,7 +7,8 @@ import toml from .constants import CONFIG_FILE, CREDENTIAL_FILE, TIDY3D_DIR -from ..environment import Env +from ..core.constants import KEY_APIKEY, HEADER_APPLICATION, HEADER_APPLICATION_VALUE +from ..core.environment import Env def migrate() -> bool: @@ -30,7 +31,7 @@ def migrate() -> bool: default=True, ) if is_migrate: - headers = {"Application": "TIDY3D"} + headers = {HEADER_APPLICATION: HEADER_APPLICATION_VALUE} resp = requests.get( f"{Env.current.web_api_endpoint}/auth", headers=headers, @@ -63,7 +64,7 @@ def migrate() -> bool: os.mkdir(TIDY3D_DIR) with open(CONFIG_FILE, "w+", encoding="utf-8") as config_file: toml_config = toml.loads(config_file.read()) - toml_config.update({"apikey": apikey}) + toml_config.update({KEY_APIKEY: apikey}) config_file.write(toml.dumps(toml_config)) # rename auth.json to auth.json.bak diff --git a/tidy3d/web/core/__init__.py b/tidy3d/web/core/__init__.py new file mode 100644 index 000000000..a30b1d9dd --- /dev/null +++ b/tidy3d/web/core/__init__.py @@ -0,0 +1 @@ +""" Tidy3d core package imports""" diff --git a/tidy3d/web/cache.py b/tidy3d/web/core/cache.py similarity index 100% rename from tidy3d/web/cache.py rename to tidy3d/web/core/cache.py diff --git a/tidy3d/web/core/constants.py b/tidy3d/web/core/constants.py new file mode 100644 index 000000000..58431c223 --- /dev/null +++ b/tidy3d/web/core/constants.py @@ -0,0 +1,27 @@ +"""Defines constants for core.""" + +# HTTP Header key and value +HEADER_APIKEY = "simcloud-api-key" +HEADER_VERSION = "tidy3d-python-version" +HEADER_SOURCE = "source" +HEADER_SOURCE_VALUE = "Python" +HEADER_USER_AGENT = "User-Agent" +HEADER_APPLICATION = "Application" +HEADER_APPLICATION_VALUE = "TIDY3D" + + +SIMCLOUD_APIKEY = "SIMCLOUD_APIKEY" +KEY_APIKEY = "apikey" +JSON_TAG = "JSON_STRING" +# type of the task_id +TaskId = str +# type of task_name +TaskName = str + + +SIMULATION_JSON = "simulation.json" +SIMULATION_DATA_HDF5 = "output/monitor_data.hdf5" +RUNNING_INFO = "output/solver_progress.csv" +SIM_LOG_FILE = "output/tidy3d.log" +SIM_FILE_HDF5 = "simulation.hdf5" +SIM_FILE_HDF5_GZ = "simulation.hdf5.gz" diff --git a/tidy3d/web/core/core_config.py b/tidy3d/web/core/core_config.py new file mode 100644 index 000000000..4ab8ed41a --- /dev/null +++ b/tidy3d/web/core/core_config.py @@ -0,0 +1,42 @@ +"""Tidy3d core log, need init config from Tidy3d api""" + +import logging as log + +# default setting +config_setting = { + "logger": log, + "logger_console": None, + "version": "", +} + + +def set_config(logger, logger_console, version: str): + """Init tidy3d core logger and logger console. + + Parameters + ---------- + logger : :class:`.Logger` + Tidy3d log Logger. + logger_console : :class:`.Console` + Get console from logging handlers. + version : str + tidy3d version + """ + config_setting["logger"] = logger + config_setting["logger_console"] = logger_console + config_setting["version"] = version + + +def get_logger(): + """Get logging handlers.""" + return config_setting["logger"] + + +def get_logger_console(): + """Get console from logging handlers.""" + return config_setting["logger_console"] + + +def get_version(): + """Get version from cache.""" + return config_setting["version"] diff --git a/tidy3d/web/core/environment.py b/tidy3d/web/core/environment.py new file mode 100644 index 000000000..39d8175b4 --- /dev/null +++ b/tidy3d/web/core/environment.py @@ -0,0 +1,168 @@ +"""Environment Setup.""" +import os +from .core_config import get_logger + +from pydantic.v1 import BaseSettings, Field + + +class EnvironmentConfig(BaseSettings): + """Basic Configuration for definition environment.""" + + def __hash__(self): + return hash((type(self),) + tuple(self.__dict__.values())) + + name: str + web_api_endpoint: str + website_endpoint: str + s3_region: str + ssl_verify: bool = Field(True, env="TIDY3D_SSL_VERIFY") + + def active(self) -> None: + """Activate the environment instance.""" + Env.set_current(self) + + def get_real_url(self, path: str) -> str: + """Get the real url for the environment instance. + + Parameters + ---------- + path : str + Base path to append to web api endpoint. + + Returns + ------- + str + Full url for the webapi. + """ + return "/".join([self.web_api_endpoint, path]) + + +dev = EnvironmentConfig( + name="dev", + s3_region="us-east-1", + web_api_endpoint="https://tidy3d-api.dev-simulation.cloud", + website_endpoint="https://tidy3d.dev-simulation.cloud", +) + +uat = EnvironmentConfig( + name="uat", + s3_region="us-gov-west-1", + web_api_endpoint="https://uat-tidy3d-api.simulation.cloud", + website_endpoint="https://uat-tidy3d.simulation.cloud", +) + +uat2 = EnvironmentConfig( + name="uat2", + s3_region="us-west-2", + web_api_endpoint="https://tidy3d-api.uat-simulation.cloud", + website_endpoint="https://tidy3d.uat-simulation.cloud", +) + +prod = EnvironmentConfig( + name="prod", + s3_region="us-gov-west-1", + web_api_endpoint="https://tidy3d-api.simulation.cloud", + website_endpoint="https://tidy3d.simulation.cloud", +) + + +class Environment: + """Environment decorator for user interactive. + + Example + ------- + >>> Env.dev.active() + >>> Env.current.name == "dev" + """ + + env_map = dict( + dev=dev, + uat=uat, + uat2=uat2, + prod=prod, + ) + + def __init__(self): + log = get_logger() + """Initialize the environment.""" + env_key = os.environ.get("TIDY3D_ENV") + env_key = env_key.lower() if env_key else env_key + log.info(f"env_key is {env_key}") + if not env_key: + self._current = prod + elif env_key in self.env_map: + self._current = self.env_map[env_key] + else: + log.warning( + f"The value '{env_key}' for the environment variable TIDY3D_ENV is not supported. " + f"Using prod as default." + ) + self._current = prod + + @property + def current(self) -> EnvironmentConfig: + """Get the current environment. + + Returns + ------- + EnvironmentConfig + The config for the current environment. + """ + return self._current + + @property + def dev(self) -> EnvironmentConfig: + """Get the dev environment. + + Returns + ------- + EnvironmentConfig + The config for the dev environment. + """ + return dev + + @property + def uat(self) -> EnvironmentConfig: + """Get the uat environment. + + Returns + ------- + EnvironmentConfig + The config for the uat environment. + """ + return uat + + @property + def uat2(self) -> EnvironmentConfig: + """Get the uat2 environment. + + Returns + ------- + EnvironmentConfig + The config for the uat environment. + """ + return uat2 + + @property + def prod(self) -> EnvironmentConfig: + """Get the prod environment. + + Returns + ------- + EnvironmentConfig + The config for the prod environment. + """ + return prod + + def set_current(self, config: EnvironmentConfig) -> None: + """Set the current environment. + + Parameters + ---------- + config : EnvironmentConfig + The environment to set to current. + """ + self._current = config + + +Env = Environment() diff --git a/tidy3d/web/core/exceptions.py b/tidy3d/web/core/exceptions.py new file mode 100644 index 000000000..873adebe9 --- /dev/null +++ b/tidy3d/web/core/exceptions.py @@ -0,0 +1,12 @@ +"""Custom Tidy3D exceptions""" +from .core_config import get_logger + + +class WebError(Exception): + """Any error in tidy3d""" + + def __init__(self, message: str = None): + """Log just the error message and then raise the Exception.""" + log = get_logger() + super().__init__(message) + log.error(message) diff --git a/tidy3d/web/core/file_util.py b/tidy3d/web/core/file_util.py new file mode 100644 index 000000000..9305eaab1 --- /dev/null +++ b/tidy3d/web/core/file_util.py @@ -0,0 +1,80 @@ +"""File compression utilities""" + +import gzip +import os +import shutil +import tempfile + +import h5py + +from ..core.constants import JSON_TAG + + +def compress_file_to_gzip(input_file, output_gz_file): + """ + Compresses a file using gzip. + + Args: + input_file (str): The path of the input file. + output_gz_file (str): The path of the output gzip file. + """ + with open(input_file, "rb") as file_in: + with gzip.open(output_gz_file, "wb") as file_out: + shutil.copyfileobj(file_in, file_out) + + +def extract_gzip_file(input_gz_file, output_file): + """ + Extract a gzip file. + + Args: + input_gz_file (str): The path of the gzip input file. + output_file (str): The path of the output file. + """ + with gzip.open(input_gz_file, "rb") as file_in: + with open(output_file, "wb") as file_out: + shutil.copyfileobj(file_in, file_out) + + +def read_simulation_from_hdf5_gz(file_name: str) -> str: + """read simulation str from hdf5.gz""" + + hdf5_file, hdf5_file_path = tempfile.mkstemp(".hdf5") + os.close(hdf5_file_path) + try: + extract_gzip_file(file_name, hdf5_file_path) + json_str = read_simulation_from_hdf5(file_name) + finally: + os.unlink(hdf5_file_path) + return json_str + + +"""TODO: _json_string_key and read_simulation_from_hdf5 are duplicated functions that also exist +as methods in Tidy3dBaseModel. For consistency it would be best if this duplication is avoided.""" + + +def _json_string_key(index): + """Get json string key for string chunk number ``index``.""" + if index: + return f"{JSON_TAG}_{index}" + return JSON_TAG + + +def read_simulation_from_hdf5(file_name: str) -> str: + """read simulation str from hdf5""" + with h5py.File(file_name, "r") as f_handle: + num_string_parts = len([key for key in f_handle.keys() if JSON_TAG in key]) + json_string = b"" + for ind in range(num_string_parts): + json_string += f_handle[_json_string_key(ind)][()] + return json_string + + +"""End TODO""" + + +def read_simulation_from_json(file_name: str) -> str: + """read simulation str from json""" + with open(file_name) as json_file: + json_data = json_file.read() + return json_data diff --git a/tidy3d/web/http_management.py b/tidy3d/web/core/http_util.py similarity index 73% rename from tidy3d/web/http_management.py rename to tidy3d/web/core/http_util.py index efa025bb3..0a6fb430e 100644 --- a/tidy3d/web/http_management.py +++ b/tidy3d/web/core/http_util.py @@ -7,15 +7,32 @@ import requests import toml -from tidy3d.web.cli.constants import CONFIG_FILE + +from .constants import ( + SIMCLOUD_APIKEY, + KEY_APIKEY, + HEADER_APIKEY, + HEADER_VERSION, + HEADER_SOURCE, + HEADER_USER_AGENT, + HEADER_APPLICATION, + HEADER_SOURCE_VALUE, + HEADER_APPLICATION_VALUE, +) from .environment import Env -from ..exceptions import WebError -from ..version import __version__ +from .exceptions import WebError +from os.path import expanduser +from . import core_config -SIMCLOUD_APIKEY = "SIMCLOUD_APIKEY" -USER_AGENT = os.environ.get("TIDY3D_AGENT", f"Python-Client/{__version__}") +TIDY3D_DIR = f"{expanduser('~')}" +if os.access(TIDY3D_DIR, os.W_OK): + TIDY3D_DIR = f"{expanduser('~')}/.tidy3d" +else: + TIDY3D_DIR = "/tmp/.tidy3d" +CONFIG_FILE = TIDY3D_DIR + "/config" +CREDENTIAL_FILE = TIDY3D_DIR + "/auth.json" class ResponseCodes(Enum): @@ -26,6 +43,16 @@ class ResponseCodes(Enum): NOT_FOUND = 404 +def get_version() -> None: + """Get the version for the current environment.""" + return core_config.get_version() + + +def get_user_agent(): + """Get the user agent the current environment.""" + return os.environ.get("TIDY3D_AGENT", f"Python-Client/{get_version()}") + + def api_key() -> None: """Get the api key for the current environment.""" @@ -34,7 +61,7 @@ def api_key() -> None: if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, encoding="utf-8") as config_file: config = toml.loads(config_file.read()) - return config.get("apikey", "") + return config.get(KEY_APIKEY, "") return None @@ -53,6 +80,7 @@ def api_key_auth(request: requests.request) -> requests.request: The request with authentication set. """ key = api_key() + version = get_version() if not key: raise ValueError( "API key not found. To get your API key, sign into 'https://tidy3d.simulation.cloud' " @@ -63,10 +91,13 @@ def api_key_auth(request: requests.request) -> requests.request: "'.tidy3d/config' (windows) containing the following line: " "apikey = 'XXX'. Here XXX is your API key copied from your account page within quotes." ) - request.headers["simcloud-api-key"] = key - request.headers["tidy3d-python-version"] = __version__ - request.headers["source"] = "Python" - request.headers["User-Agent"] = USER_AGENT + if not version: + raise ValueError("version not found.") + + request.headers[HEADER_APIKEY] = key + request.headers[HEADER_VERSION] = version + request.headers[HEADER_SOURCE] = HEADER_SOURCE_VALUE + request.headers[HEADER_USER_AGENT] = get_user_agent() return request @@ -78,7 +109,11 @@ def get_headers() -> Dict[str, str]: Dict[str, str] dictionary with "Authorization" and "Application" keys. """ - return {"simcloud-api-key": api_key(), "Application": "TIDY3D", "User-Agent": USER_AGENT} + return { + HEADER_APIKEY: api_key(), + HEADER_APPLICATION: HEADER_APPLICATION_VALUE, + HEADER_USER_AGENT: get_user_agent(), + } def http_interceptor(func): diff --git a/tidy3d/web/s3utils.py b/tidy3d/web/core/s3utils.py similarity index 98% rename from tidy3d/web/s3utils.py rename to tidy3d/web/core/s3utils.py index 1b1ce8c31..72cde3aa8 100644 --- a/tidy3d/web/s3utils.py +++ b/tidy3d/web/core/s3utils.py @@ -11,9 +11,9 @@ from pydantic.v1 import BaseModel, Field from rich.progress import TextColumn, Progress, BarColumn, DownloadColumn from rich.progress import TransferSpeedColumn, TimeRemainingColumn -from ..log import get_logging_console -from .http_management import http +from .http_util import http from .environment import Env +from .core_config import get_logger_console class _UserCredential(BaseModel): @@ -158,7 +158,7 @@ def _get_progress(action: _S3Action): TransferSpeedColumn(), "•", TimeRemainingColumn(), - console=get_logging_console(), + console=get_logger_console(), ) diff --git a/tidy3d/web/core/stub.py b/tidy3d/web/core/stub.py new file mode 100644 index 000000000..9c27a5fff --- /dev/null +++ b/tidy3d/web/core/stub.py @@ -0,0 +1,84 @@ +"""Defines interface that can be subclassed to use with the tidy3d webapi""" +from __future__ import annotations + +from abc import ABC, abstractmethod + + +class TaskStubData(ABC): + @abstractmethod + def from_file(self, file_path) -> TaskStubData: + """Loads a :class:`TaskStubData` from .yaml, .json, or .hdf5 file. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to load the :class:`Stub` from. + + Returns + ------- + :class:`Stub` + An instance of the component class calling `load`. + + """ + pass + + @abstractmethod + def to_file(self, file_path): + """Loads a :class:`Stub` from .yaml, .json, or .hdf5 file. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to load the :class:`Stub` from. + + Returns + ------- + :class:`Stub` + An instance of the component class calling `load`. + """ + pass + + +class TaskStub(ABC): + @abstractmethod + def from_file(self, file_path) -> TaskStub: + """Loads a :class:`TaskStubData` from .yaml, .json, or .hdf5 file. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to load the :class:`Stub` from. + + Returns + ------- + :class:`TaskStubData` + An instance of the component class calling `load`. + """ + pass + + @abstractmethod + def to_file(self, file_path): + """Loads a :class:`TaskStub` from .yaml, .json, .hdf5 or .hdf5.gz file. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to load the :class:`TaskStub` from. + + Returns + ------- + :class:`Stub` + An instance of the component class calling `load`. + """ + pass + + @abstractmethod + def to_hdf5_gz(self, fname: str) -> None: + """Exports :class:`TaskStub` instance to .hdf5.gz file. + + Parameters + ---------- + fname : str + Full path to the .hdf5.gz file to save the :class:`TaskStub` to. + """ + pass diff --git a/tidy3d/web/simulation_task.py b/tidy3d/web/core/task_core.py similarity index 79% rename from tidy3d/web/simulation_task.py rename to tidy3d/web/core/task_core.py index bc5c17dd5..0c2dd8cbd 100644 --- a/tidy3d/web/simulation_task.py +++ b/tidy3d/web/core/task_core.py @@ -8,36 +8,21 @@ from typing import List, Optional, Callable, Tuple import pydantic.v1 as pd from pydantic.v1 import Extra, Field, parse_obj_as -import h5py -from tidy3d import Simulation -from tidy3d.version import __version__ -from tidy3d.exceptions import WebError, DataError -from tidy3d.components.file_util import extract_gzip_file -from tidy3d.components.base import JSON_TAG +from . import http_util +from .core_config import get_logger_console +from .exceptions import WebError from .cache import FOLDER_CACHE -from .http_management import http +from .http_util import http from .s3utils import download_file, upload_file +from .stub import TaskStub from .types import Queryable, ResourceLifecycle, Submittable from .types import Tidy3DResource -from ..log import get_logging_console -SIMULATION_JSON = "simulation.json" -SIMULATION_DATA_HDF5 = "output/monitor_data.hdf5" -RUNNING_INFO = "output/solver_progress.csv" -SIM_LOG_FILE = "output/tidy3d.log" -SIM_FILE_HDF5 = "simulation.hdf5" -SIM_FILE_HDF5_GZ = "simulation.hdf5.gz" - - -def _read_simulation_from_hdf5(file_name: str): - """read simulation str from hdf5""" - - with h5py.File(file_name, "r") as f_handle: - json_string = f_handle[JSON_TAG][()] - return json_string +from .constants import SIM_FILE_HDF5_GZ, SIMULATION_DATA_HDF5, SIM_LOG_FILE +from .file_util import extract_gzip_file, read_simulation_from_hdf5 class Folder(Tidy3DResource, Queryable, extra=Extra.allow): @@ -142,6 +127,12 @@ class SimulationTask(ResourceLifecycle, Submittable, extra=Extra.allow): description="Task ID number, set when the task is uploaded, leave as None.", alias="taskId", ) + folder_id: Optional[str] = Field( + None, + title="folder_id", + description="Folder ID number, set when the task is uploaded, leave as None.", + alias="projectId", + ) status: Optional[str] = Field(title="status", description="Simulation task status.") real_flex_unit: float = Field( @@ -152,8 +143,8 @@ class SimulationTask(ResourceLifecycle, Submittable, extra=Extra.allow): title="created_at", description="Time at which this task was created.", alias="createdAt" ) - simulation: Optional[Simulation] = Field( - title="simulation", description="A copy of the Simulation being run as this task." + task_type: Optional[str] = Field( + title="task_type", description="The type of task.", alias="taskType" ) folder_name: Optional[str] = Field( @@ -163,8 +154,6 @@ class SimulationTask(ResourceLifecycle, Submittable, extra=Extra.allow): alias="projectName", ) - folder: Optional[Folder] - callback_url: str = Field( None, title="Callback URL", @@ -202,7 +191,7 @@ def _error_if_jax_sim(cls, values): @classmethod def create( cls, - simulation: Simulation, + task_type: str, task_name: str, folder_name: str = "default", callback_url: str = None, @@ -214,10 +203,8 @@ def create( Parameters ---------- - simulation: :class".Simulation" - The :class:`.Simulation` will be uploaded to server in the submitting phase. - If Simulation is too large to fit into memory, pass None to this parameter - and use :meth:`.SimulationTask.upload_file` instead. + task_type: :class".TaskType" + The type of task. task_name: str The name of the task. folder_name: str, @@ -243,23 +230,25 @@ def create( f"tidy3d/projects/{folder.folder_id}/tasks", { "taskName": task_name, + "taskType": task_type, "callbackUrl": callback_url, "simulationType": simulation_type, "parentTasks": parent_tasks, "fileType": file_type, }, ) - - return SimulationTask(**resp, simulation=simulation, folder=folder) + return SimulationTask(**resp, taskType=task_type) @classmethod - def get(cls, task_id: str) -> SimulationTask: + def get(cls, task_id: str, verbose: bool = True) -> SimulationTask: """Get task from the server by id. Parameters ---------- task_id: str Unique identifier of task on server. + verbose: + If `True`, will print progressbars and status, otherwise, will run silently. Returns ------- @@ -268,7 +257,8 @@ def get(cls, task_id: str) -> SimulationTask: size, credits of task and others. """ resp = http.get(f"tidy3d/tasks/{task_id}/detail") - return SimulationTask(**resp) if resp else None + task = SimulationTask(**resp) if resp else None + return task @classmethod def get_running_tasks(cls) -> List[SimulationTask]: @@ -291,25 +281,6 @@ def delete(self): raise ValueError("Task id not found.") http.delete(f"tidy3d/tasks/{self.task_id}") - def get_simulation(self) -> Optional[Simulation]: - """Download simulation from server. - - Returns - ------- - :class:`.Simulation` - :class:`.Simulation` object containing info about status, size, - credits of task and others. - """ - if self.simulation: - return self.simulation - - with tempfile.NamedTemporaryFile(suffix=".json") as temp: - self.get_simulation_json(temp.name) - if pathlib.Path(temp.name).exists(): - self.simulation = Simulation.from_file(temp.name) - return self.simulation - return None - def get_simulation_json(self, to_file: str, verbose: bool = True) -> pathlib.Path: """Get json file for a :class:`.Simulation` from server. @@ -326,19 +297,19 @@ def get_simulation_json(self, to_file: str, verbose: bool = True) -> pathlib.Pat Path to saved file. """ if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") + raise WebError("Expected field 'task_id' is unset.") hdf5_file, hdf5_file_path = tempfile.mkstemp(".hdf5") os.close(hdf5_file) try: self.get_simulation_hdf5(hdf5_file_path) if os.path.exists(hdf5_file_path): - json_string = _read_simulation_from_hdf5(hdf5_file_path) + json_string = read_simulation_from_hdf5(hdf5_file_path) with open(to_file, "w") as file: # Write the string to the file file.write(json_string.decode("utf-8")) if verbose: - console = get_logging_console() + console = get_logger_console() console.log(f"Generate {to_file} successfully.") else: raise WebError("Failed to download simulation.json.") @@ -346,27 +317,33 @@ def get_simulation_json(self, to_file: str, verbose: bool = True) -> pathlib.Pat os.unlink(hdf5_file_path) def upload_simulation( - self, verbose: bool = True, progress_callback: Callable[[float], None] = None + self, + stub: TaskStub, + verbose: bool = True, + progress_callback: Callable[[float], None] = None, ) -> None: """Upload :class:`.Simulation` object to Server. Parameters ---------- + stub: :class:`TaskStub` + An instance of TaskStub. verbose: bool = True Whether to display progress bars. progress_callback : Callable[[float], None] = None Optional callback function called while uploading the data. """ if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") - if not self.simulation: - raise DataError("Expected field 'simulation' is unset.") - - # Upload hdf5.gz containing all data. - file, file_name = tempfile.mkstemp(".hdf5.gz") + raise WebError("Expected field 'task_id' is unset.") + if not stub: + raise WebError("Expected field 'simulation' is unset.") + # Also upload hdf5.gz containing all data. + file, file_name = tempfile.mkstemp() os.close(file) try: - self.simulation.to_hdf5_gz(file_name) + # upload simulation + # compress .hdf5 to .hdf5.gz + stub.to_hdf5_gz(file_name) upload_file( self.task_id, file_name, @@ -399,7 +376,7 @@ def upload_file( Optional callback function called while uploading the data. """ if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") + raise WebError("Expected field 'task_id' is unset.") upload_file( self.task_id, @@ -416,7 +393,7 @@ def submit( ): """Kick off this task. - If this task instance contain a :class:`.Simulation`, it will be uploaded to server before + It will be uploaded to server before starting the task. Otherwise, this method assumes that the Simulation has been uploaded by the upload_file function, so the task will be kicked off directly. @@ -427,26 +404,11 @@ def submit( worker_group: str = None worker group """ - if self.simulation: - # Also upload hdf5.gz containing all data. - file, file_name = tempfile.mkstemp(".hdf5.gz") - os.close(file) - try: - self.simulation.to_hdf5_gz(file_name) - upload_file( - self.task_id, - file_name, - SIM_FILE_HDF5_GZ, - verbose=False, - progress_callback=None, - ) - finally: - os.unlink(file_name) if solver_version: protocol_version = None else: - protocol_version = __version__ + protocol_version = http_util.get_version() http.post( f"tidy3d/tasks/{self.task_id}/submit", @@ -471,14 +433,13 @@ def estimate_cost(self, solver_version=None) -> float: flex_unit_cost: float estimated cost in FlexCredits """ + if not self.task_id: + raise WebError("Expected field 'task_id' is unset.") if solver_version: protocol_version = None else: - protocol_version = __version__ - - if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") + protocol_version = http_util.get_version() resp = http.post( f"tidy3d/tasks/{self.task_id}/metadata", @@ -509,15 +470,20 @@ def get_sim_data_hdf5( Path to saved file. """ if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") + raise WebError("Expected field 'task_id' is unset.") - return download_file( - self.task_id, - SIMULATION_DATA_HDF5, - to_file=to_file, - verbose=verbose, - progress_callback=progress_callback, - ) + try: + return download_file( + self.task_id, + SIMULATION_DATA_HDF5, + to_file=to_file, + verbose=verbose, + progress_callback=progress_callback, + ) + except Exception: + raise WebError( + f"Failed to download file '{SIMULATION_DATA_HDF5}' from server. Please confirm that the task was successful." + ) def get_simulation_hdf5( self, to_file: str, verbose: bool = True, progress_callback: Callable[[float], None] = None @@ -539,7 +505,7 @@ def get_simulation_hdf5( Path to saved file. """ if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") + raise WebError("Expected field 'task_id' is unset.") if to_file.lower().endswith(".gz"): download_file( @@ -580,7 +546,7 @@ def get_running_info(self) -> Tuple[float, float]: Is ``None`` if run info not available. """ if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") + raise WebError("Expected field 'task_id' is unset.") resp = http.get(f"tidy3d/tasks/{self.task_id}/progress") perc_done = resp.get("perc_done") @@ -608,7 +574,7 @@ def get_log( """ if not self.task_id: - raise DataError("Expected field 'task_id' is unset.") + raise WebError("Expected field 'task_id' is unset.") return download_file( self.task_id, @@ -622,4 +588,6 @@ def abort(self): """Abort current task from server.""" if not self.task_id: raise ValueError("Task id not found.") - return http.put("tidy3d/tasks/abort", json={"taskType": "FDTD", "taskId": self.task_id}) + return http.put( + "tidy3d/tasks/abort", json={"taskType": self.task_type, "taskId": self.task_id} + ) diff --git a/tidy3d/web/task.py b/tidy3d/web/core/task_info.py similarity index 87% rename from tidy3d/web/task.py rename to tidy3d/web/core/task_info.py index 81351c6ee..f1714db3a 100644 --- a/tidy3d/web/task.py +++ b/tidy3d/web/core/task_info.py @@ -28,13 +28,6 @@ class Config: arbitrary_types_allowed = True -# type of the task_id -TaskId = str - -# type of task_name -TaskName = str - - class ChargeType(str, Enum): """The payment method of task.""" @@ -67,7 +60,11 @@ class TaskInfo(TaskBase): estCostMin: float = None estCostMax: float = None realFlexUnit: float = None + oriRealFlexUnit: float = None estFlexUnit: float = None + estFlexCreditTimeStepping: float = None + estFlexCreditPostProcess: float = None + estFlexCreditMode: float = None s3Storage: float = None startSolverTime: Optional[datetime] = None finishSolverTime: Optional[datetime] = None @@ -88,15 +85,3 @@ def display(self): """Print some info.""" print(f" - {self.perc_done:.2f} (%) done") print(f" - {self.field_decay:.2e} field decay from max") - - -class Folder(pydantic.BaseModel): - """Folder information of a task.""" - - projectName: str = None - projectId: str = None - - class Config: - """Configure class.""" - - arbitrary_types_allowed = True diff --git a/tidy3d/web/types.py b/tidy3d/web/core/types.py similarity index 88% rename from tidy3d/web/types.py rename to tidy3d/web/core/types.py index 0e2254c4f..9e03eea66 100644 --- a/tidy3d/web/types.py +++ b/tidy3d/web/core/types.py @@ -1,9 +1,10 @@ -"""Tidy3d abstraction types for the webapi.""" +"""Tidy3d abstraction types for the core.""" from __future__ import annotations from abc import ABC, abstractmethod from pydantic.v1 import BaseModel +from enum import Enum class Tidy3DResource(BaseModel, ABC): @@ -43,3 +44,9 @@ class Queryable(BaseModel, ABC): @abstractmethod def list(cls, *args, **kwargs) -> [Queryable]: """List all resources of this type.""" + + +class TaskType(str, Enum): + FDTD = "FDTD" + MODE_SOLVER = "MODE_SOLVER" + HEAT = "HEAT" diff --git a/tidy3d/web/environment.py b/tidy3d/web/environment.py index 0c5458492..a1643b093 100644 --- a/tidy3d/web/environment.py +++ b/tidy3d/web/environment.py @@ -1,168 +1,4 @@ -"""Environment Setup.""" -import os +""" preserve from tidy3d.web.environment import Env backward compatibility """ +from .core.environment import Env -from pydantic.v1 import BaseSettings, Field - -from tidy3d import log - - -class EnvironmentConfig(BaseSettings): - """Basic Configuration for definition environment.""" - - def __hash__(self): - return hash((type(self),) + tuple(self.__dict__.values())) - - name: str - web_api_endpoint: str - website_endpoint: str - s3_region: str - ssl_verify: bool = Field(True, env="TIDY3D_SSL_VERIFY") - - def active(self) -> None: - """Activate the environment instance.""" - Env.set_current(self) - - def get_real_url(self, path: str) -> str: - """Get the real url for the environment instance. - - Parameters - ---------- - path : str - Base path to append to web api endpoint. - - Returns - ------- - str - Full url for the webapi. - """ - return "/".join([self.web_api_endpoint, path]) - - -dev = EnvironmentConfig( - name="dev", - s3_region="us-east-1", - web_api_endpoint="https://tidy3d-api.dev-simulation.cloud", - website_endpoint="https://tidy3d.dev-simulation.cloud", -) - -uat = EnvironmentConfig( - name="uat", - s3_region="us-gov-west-1", - web_api_endpoint="https://uat-tidy3d-api.simulation.cloud", - website_endpoint="https://uat-tidy3d.simulation.cloud", -) - -uat2 = EnvironmentConfig( - name="uat2", - s3_region="us-west-2", - web_api_endpoint="https://tidy3d-api.uat-simulation.cloud", - website_endpoint="https://tidy3d.uat-simulation.cloud", -) - -prod = EnvironmentConfig( - name="prod", - s3_region="us-gov-west-1", - web_api_endpoint="https://tidy3d-api.simulation.cloud", - website_endpoint="https://tidy3d.simulation.cloud", -) - - -class Environment: - """Environment decorator for user interactive. - - Example - ------- - >>> Env.dev.active() - >>> Env.current.name == "dev" - """ - - env_map = dict( - dev=dev, - uat=uat, - uat2=uat2, - prod=prod, - ) - - def __init__(self): - """Initialize the environment.""" - env_key = os.environ.get("TIDY3D_ENV") - env_key = env_key.lower() if env_key else env_key - log.info(f"env_key is {env_key}") - if not env_key: - self._current = prod - elif env_key in self.env_map: - self._current = self.env_map[env_key] - else: - log.warning( - f"The value '{env_key}' for the environment variable TIDY3D_ENV is not supported. " - f"Using prod as default." - ) - self._current = prod - - @property - def current(self) -> EnvironmentConfig: - """Get the current environment. - - Returns - ------- - EnvironmentConfig - The config for the current environment. - """ - return self._current - - @property - def dev(self) -> EnvironmentConfig: - """Get the dev environment. - - Returns - ------- - EnvironmentConfig - The config for the dev environment. - """ - return dev - - @property - def uat(self) -> EnvironmentConfig: - """Get the uat environment. - - Returns - ------- - EnvironmentConfig - The config for the uat environment. - """ - return uat - - @property - def uat2(self) -> EnvironmentConfig: - """Get the uat2 environment. - - Returns - ------- - EnvironmentConfig - The config for the uat environment. - """ - return uat2 - - @property - def prod(self) -> EnvironmentConfig: - """Get the prod environment. - - Returns - ------- - EnvironmentConfig - The config for the prod environment. - """ - return prod - - def set_current(self, config: EnvironmentConfig) -> None: - """Set the current environment. - - Parameters - ---------- - config : EnvironmentConfig - The environment to set to current. - """ - self._current = config - - -Env = Environment() +__all__ = ["Env"] diff --git a/tox.ini b/tox.ini index d137603a0..ac0b2cc14 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] envlist = - python3.7 python3.8 python3.9 python3.10 @@ -8,7 +7,6 @@ envlist = [gh-actions] python = - 3.7: python3.7 3.8: python3.8 3.9: python3.9 3.10: python3.10 @@ -25,8 +23,10 @@ deps = -rrequirements/gdstk.txt -rrequirements/dev.txt -rrequirements/trimesh.txt + -rrequirements/vtk.txt commands = pip install requests black . --check --diff ruff check tidy3d --fix --exit-non-zero-on-fix pytest -rA tests + pytest -rA tests/test_data/_test_datasets_no_vtk.py