From 13eb08aa7001fd8dace75ab5a28359a33cd0f147 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Fri, 22 Sep 2023 10:13:32 -0400 Subject: [PATCH 01/83] pre/2.5 branch --- CHANGELOG.md | 8 ++++++++ tidy3d/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f2fc038d..6258dbfd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ 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] + +### Added + +### Changed + +### Fixed + ## [2.4.1] - 2023-9-20 ### Added diff --git a/tidy3d/version.py b/tidy3d/version.py index a9287cb69..658327077 100644 --- a/tidy3d/version.py +++ b/tidy3d/version.py @@ -1,3 +1,3 @@ """Defines the front end version of tidy3d""" -__version__ = "2.4.1" +__version__ = "2.5.0rc1" From 789cffa6f252867d799e9032fa9bc499dd344187 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Thu, 21 Sep 2023 15:58:28 -0500 Subject: [PATCH 02/83] web api stub refactor --- CHANGELOG.md | 1 + tests/test_plugins/test_adjoint.py | 2 +- tests/test_plugins/test_component_modeler.py | 2 +- tests/test_plugins/test_mode_solver.py | 15 +- tests/test_web/test_material_fitter.py | 19 +- tests/test_web/test_task.py | 2 +- tests/test_web/test_tidy3d_folder.py | 25 +- .../test_web/test_tidy3d_material_library.py | 10 +- tests/test_web/test_tidy3d_task.py | 71 ++- tests/test_web/test_webapi.py | 137 +++-- tidy3d/plugins/adjoint/web.py | 10 +- tidy3d/plugins/dispersion/fit.py | 2 +- tidy3d/plugins/dispersion/web.py | 4 +- tidy3d/plugins/mode/mode_solver.py | 5 +- tidy3d/plugins/mode/web.py | 470 +---------------- tidy3d/plugins/smatrix/smatrix.py | 2 +- tidy3d/web/__init__.py | 22 +- tidy3d/web/api/__init__.py | 0 tidy3d/web/{ => api}/asynchronous.py | 13 +- tidy3d/web/{ => api}/cacert.pem | 0 tidy3d/web/api/connect_util.py | 68 +++ tidy3d/web/{ => api}/container.py | 80 +-- tidy3d/web/{ => api}/material_fitter.py | 6 +- tidy3d/web/{ => api}/material_libray.py | 6 +- tidy3d/web/api/mode.py | 471 ++++++++++++++++++ tidy3d/web/api/tidy3d_stub.py | 200 ++++++++ tidy3d/web/{ => api}/webapi.py | 188 +++---- tidy3d/web/cli/app.py | 13 +- tidy3d/web/cli/migrate.py | 7 +- tidy3d/web/core/__init__.py | 1 + tidy3d/web/{ => core}/cache.py | 0 tidy3d/web/core/constants.py | 27 + tidy3d/web/core/core_config.py | 42 ++ tidy3d/web/{ => core}/environment.py | 4 +- tidy3d/web/core/exceptions.py | 12 + tidy3d/web/core/file_util.py | 64 +++ .../{http_management.py => core/http_util.py} | 57 ++- tidy3d/web/{ => core}/s3utils.py | 6 +- tidy3d/web/core/stub.py | 84 ++++ .../{simulation_task.py => core/task_core.py} | 142 ++---- tidy3d/web/{task.py => core/task_info.py} | 19 - tidy3d/web/{ => core}/types.py | 9 +- 42 files changed, 1425 insertions(+), 893 deletions(-) create mode 100644 tidy3d/web/api/__init__.py rename tidy3d/web/{ => api}/asynchronous.py (83%) rename tidy3d/web/{ => api}/cacert.pem (100%) create mode 100644 tidy3d/web/api/connect_util.py rename tidy3d/web/{ => api}/container.py (88%) rename tidy3d/web/{ => api}/material_fitter.py (96%) rename tidy3d/web/{ => api}/material_libray.py (92%) create mode 100644 tidy3d/web/api/mode.py create mode 100644 tidy3d/web/api/tidy3d_stub.py rename tidy3d/web/{ => api}/webapi.py (81%) create mode 100644 tidy3d/web/core/__init__.py rename tidy3d/web/{ => core}/cache.py (100%) create mode 100644 tidy3d/web/core/constants.py create mode 100644 tidy3d/web/core/core_config.py rename tidy3d/web/{ => core}/environment.py (98%) create mode 100644 tidy3d/web/core/exceptions.py create mode 100644 tidy3d/web/core/file_util.py rename tidy3d/web/{http_management.py => core/http_util.py} (73%) rename tidy3d/web/{ => core}/s3utils.py (98%) create mode 100644 tidy3d/web/core/stub.py rename tidy3d/web/{simulation_task.py => core/task_core.py} (81%) rename tidy3d/web/{task.py => core/task_info.py} (87%) rename tidy3d/web/{ => core}/types.py (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6258dbfd3..f38530259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed +- Internal refactor of Web API functionality. ### Fixed diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index 4360fd5fa..7a6170392 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -32,7 +32,7 @@ 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 from ..utils import run_emulated, assert_log_level, log_capture, run_async_emulated diff --git a/tests/test_plugins/test_component_modeler.py b/tests/test_plugins/test_component_modeler.py index a24154bcf..39f863136 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 diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 5942c9b75..2cd9f6c66 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -6,7 +6,6 @@ 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 @@ -14,8 +13,7 @@ from tidy3d.plugins.mode.solver import compute_modes 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 +62,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 +88,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__, } ) ], 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..a2edc26ab 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/tidy3d/plugins/adjoint/web.py b/tidy3d/plugins/adjoint/web.py index 16abcf0b5..23c97e6f4 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 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/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..fcda3c079 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -27,7 +27,6 @@ from ...constants import C_0 from .solver import compute_modes - FIELD = Tuple[ArrayComplex3D, ArrayComplex3D, ArrayComplex3D] MODE_MONITOR_NAME = "<<>>" @@ -403,7 +402,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 ) @@ -800,3 +798,6 @@ def plot_field( ax=ax, **sel_kwargs, ) + + def validate_pre_upload(self, source_required: bool = True): + pass 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..85a67108c 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 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 83% rename from tidy3d/web/asynchronous.py rename to tidy3d/web/api/asynchronous.py index c127efce2..038c5b45a 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, + """Submits a set of Union[:class:`.Simulation`] 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`]] 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`] for each Union[:class:`.Simulation`] + 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 88% rename from tidy3d/web/container.py rename to tidy3d/web/api/container.py index 6321c5601..32142aec7 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,10 @@ class WebContainer(Tidy3dBaseModel, ABC): class Job(WebContainer): - """Interface for managing the running of a :class:`.Simulation` on server.""" + """Interface for managing the running of Union[:class:`.Simulation`] on 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'." ) task_name: TaskName = pd.Field(..., title="Task Name", description="Unique name of the task.") @@ -85,8 +84,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 +94,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`. + Dict[str, Union[:class:`.SimulationData`]] + Dictionary mapping task name to Union[:class:`.SimulationData`] for :class:`Job`. """ self.start() @@ -174,13 +173,13 @@ 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 into Union[:class:`.SimulationData`] objets, can call :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 results from simulation (if not already) and load them into + Union[:class:`.SimulationData`] object. Parameters ---------- @@ -189,8 +188,8 @@ def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationData: Returns ------- - :class:`.SimulationData` - Object containing data about simulation. + Union[:class:`.SimulationData`] + Object containing simulation results. """ return web.load(task_id=self.task_id, path=path, verbose=self.verbose) @@ -208,7 +207,8 @@ def estimate_cost(self) -> float: Returns ------- float - Estimated maximum cost for :class:`.Simulation` associated with given ``Job``. + Estimated maximum cost for Union[:class:`.Simulation`] associated with + the given :class:`.Job`. Note ---- @@ -224,7 +224,7 @@ def estimate_cost(self) -> float: class BatchData(Tidy3dBaseModel): - """Holds a collection of :class:`.SimulationData` returned by :class:`.Batch`.""" + """Holds a collection of Union[:class:`.SimulationData`] returned by :class:`.Batch`.""" task_paths: Dict[TaskName, str] = pd.Field( ..., @@ -240,8 +240,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 Union[:class:`.SimulationData`] 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 +252,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 Union[:class:`.SimulationData`] 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 Union[:class:`.SimulationData`] for a given ``task_name``.""" return self.load_sim_data(task_name) @classmethod @@ -274,7 +274,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`] for each Union[:class:`.Simulation`] + in :class:`Batch`. """ batch_file = Batch._batch_path(path_dir=path_dir) @@ -283,9 +284,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 several Union[:class:`.Simulation`] objects to 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 +354,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`] of each Union[:class:`.Simulation`] + in :class:`Batch`. Note ---- @@ -363,9 +365,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, + ``bach_data`` does not store all of the Union[:class:`.SimulationData`] objects in memory, rather it iterates over the task names - and loads the corresponding :class:`.SimulationData` from file one by one. + and loads the corresponding Union[:class:`.SimulationData`] from file one by one. If no file exists for that task, it downloads it. """ self._check_path_dir(path_dir) @@ -570,7 +572,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 the data into Union[:class:`.SimulationData`] objets, can call :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 +601,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`] of each Union[:class:`.Simulation`] + 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()``. @@ -642,7 +645,8 @@ def estimate_cost(self) -> float: Returns ------- float - Estimated maximum cost for each :class:`.Simulation` associated with given ``Batch``. + Estimated maximum cost for each Union[:class:`.Simulation`] associated with + given :class:`.Batch`. Note ---- 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..57f7dc43c --- /dev/null +++ b/tidy3d/web/api/mode.py @@ -0,0 +1,471 @@ +"""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.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, + ) diff --git a/tidy3d/web/api/tidy3d_stub.py b/tidy3d/web/api/tidy3d_stub.py new file mode 100644 index 000000000..2f2d4008c --- /dev/null +++ b/tidy3d/web/api/tidy3d_stub.py @@ -0,0 +1,200 @@ +"""Stub for webapi""" +from __future__ import annotations + +import json +from typing import Union, Callable, List + +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 + + +SimulationType = Union[Simulation] +SimulationDataType = Union[SimulationData] + + +class Tidy3dStub(BaseModel, TaskStub): + + simulation: SimulationType + + @classmethod + def from_file(cls, file_path: str) -> SimulationType: + """Loads a Union[:class:`.Simulation`] 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`] from. + + Returns + ------- + Union[:class:`.Simulation`] + 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) + + return sim + + def to_file( + self, + file_path: str, + ): + """Exports Union[:class:`.Simulation`] 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`] instance to .hdf5.gz file. + + Parameters + ---------- + fname : str + Full path to the .hdf5.gz file to save the Union[:class:`.Simulation`] 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 + + 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`] 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`] from. + + Returns + ------- + Union[:class:`.SimulationData`] + 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) + + return sim_data + + def to_file(self, file_path: str): + """Exports Union[:class:`.SimulationData`] 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`] 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`] instance. + + Parameters + ---------- + file_path : str + Full path to the .yaml or .json or .hdf5 file to save the + Union[:class:`.SimulationData`] to. + + Returns + ------- + Union[:class:`.SimulationData`] + 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} " + f"is greater than the simulation shutoff threshold of {shutoff_value}. " + "Consider simulation again with large 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 81% rename from tidy3d/web/webapi.py rename to tidy3d/web/api/webapi.py index 8de752f42..28c1f3621 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 Union[:class:`.Simulation`] to server, starts running, monitors progress, + downloads, and loads results as a corresponding Union[:class:`.SimulationData`] object. Parameters ---------- - simulation : :class:`.Simulation` + simulation : Union[:class:`.Simulation`] 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`] + 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 simulation to server, but do not start running Union[:class:`.Simulation`]. Parameters ---------- - simulation : :class:`.Simulation` + simulation : Union[:class:`.Simulation`] Simulation to upload to server. task_name : str Name of task. @@ -181,45 +144,49 @@ 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}'.") + console.log( + f"Created task '{task_name}' with task_id '{task.task_id}' and task_type '{task_type}'." + ) 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"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 +214,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 +235,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 +260,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 @@ -304,7 +273,6 @@ 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 @@ -320,20 +288,8 @@ def get_estimated_cost() -> float: 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" - - 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" - + grid_points_str = get_grid_points_str(grid_points) + time_steps_str = get_time_steps_str(time_steps) 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" @@ -508,8 +464,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 Union[:class:`.Simulation`]. Parameters ---------- @@ -522,15 +480,13 @@ def load_simulation(task_id: TaskId, path: str = SIM_FILE_JSON, verbose: bool = Returns ------- - :class:`.Simulation` - Simulation loaded from downloaded json file. + Union[:class:`.Simulation`] + SimulationType 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 +524,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 Union[:class:`.SimulationData`] object. Parameters ---------- @@ -586,29 +542,18 @@ def load( Returns ------- - :class:`.SimulationData` + Union[:class:`.SimulationData`] 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 +612,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 +626,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" diff --git a/tidy3d/web/cli/app.py b/tidy3d/web/cli/app.py index 5858c0d24..3ac6cce54 100644 --- a/tidy3d/web/cli/app.py +++ b/tidy3d/web/cli/app.py @@ -9,9 +9,10 @@ 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 ..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 if not os.path.exists(TIDY3D_DIR): os.mkdir(TIDY3D_DIR) @@ -29,7 +30,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 "" @@ -73,7 +74,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): @@ -102,7 +103,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/environment.py b/tidy3d/web/core/environment.py similarity index 98% rename from tidy3d/web/environment.py rename to tidy3d/web/core/environment.py index 0c5458492..39d8175b4 100644 --- a/tidy3d/web/environment.py +++ b/tidy3d/web/core/environment.py @@ -1,10 +1,9 @@ """Environment Setup.""" import os +from .core_config import get_logger from pydantic.v1 import BaseSettings, Field -from tidy3d import log - class EnvironmentConfig(BaseSettings): """Basic Configuration for definition environment.""" @@ -84,6 +83,7 @@ class Environment: ) 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 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..bd3bbdc9b --- /dev/null +++ b/tidy3d/web/core/file_util.py @@ -0,0 +1,64 @@ +"""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 + + +def read_simulation_from_hdf5(file_name: str) -> str: + """read simulation str from hdf5""" + + with h5py.File(file_name, "r") as f_handle: + json_string = f_handle[JSON_TAG][()] + return json_string + + +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 81% rename from tidy3d/web/simulation_task.py rename to tidy3d/web/core/task_core.py index bc5c17dd5..1c59f0728 100644 --- a/tidy3d/web/simulation_task.py +++ b/tidy3d/web/core/task_core.py @@ -10,26 +10,20 @@ 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" +from .constants import SIM_FILE_HDF5_GZ, SIMULATION_DATA_HDF5, SIM_LOG_FILE, JSON_TAG +from .file_util import extract_gzip_file def _read_simulation_from_hdf5(file_name: str): @@ -142,6 +136,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 +152,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 +163,6 @@ class SimulationTask(ResourceLifecycle, Submittable, extra=Extra.allow): alias="projectName", ) - folder: Optional[Folder] - callback_url: str = Field( None, title="Callback URL", @@ -202,7 +200,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 +212,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 +239,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 +266,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 +290,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,7 +306,7 @@ 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) @@ -338,7 +318,7 @@ def get_simulation_json(self, to_file: str, verbose: bool = True) -> pathlib.Pat # 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 +326,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 +385,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 +402,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,33 +413,12 @@ 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__ - http.post( f"tidy3d/tasks/{self.task_id}/submit", { "solverVersion": solver_version, "workerGroup": worker_group, - "protocolVersion": protocol_version, + "protocolVersion": http_util.get_version(), }, ) @@ -471,14 +436,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,7 +473,7 @@ 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, @@ -539,7 +503,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 +544,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 +572,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 +586,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..2684ae9d2 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.""" @@ -88,15 +81,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" From 0d524cbdfe7571a2cf75d1883c72e54ba4e96773 Mon Sep 17 00:00:00 2001 From: Lucas <119979961+lucas-flexcompute@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:53:45 -0300 Subject: [PATCH 03/83] Avoid creating unecessary geometry groups of single elements (#1177) Signed-off-by: Lucas Heitzmann Gabrielli --- CHANGELOG.md | 2 +- tidy3d/components/geometry/base.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f38530259..65cd6d21f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Internal refactor of Web API functionality. +- `Geometry.from_gds` doesn't create unecessary groups of single elements. ### Fixed @@ -31,7 +32,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 diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index 8e7c6061e..c2a789821 100644 --- a/tidy3d/components/geometry/base.py +++ b/tidy3d/components/geometry/base.py @@ -885,7 +885,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 @@ -917,8 +917,8 @@ 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 @@ -951,7 +951,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( From e155c4f05249e20cafa5977a545621129f8dbdc6 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Thu, 28 Sep 2023 13:25:40 -0400 Subject: [PATCH 04/83] time zone in webapi log --- CHANGELOG.md | 1 + tidy3d/log.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65cd6d21f..1e9ea4714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Time zone in webAPI logging output. ### Changed - Internal refactor of Web API functionality. diff --git a/tidy3d/log.py b/tidy3d/log.py index 21ade0909..f38f37912 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, ) From 9b11226b49d4d04af1f90d40f318db3b8ed65fe7 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Thu, 5 Oct 2023 23:11:17 -0500 Subject: [PATCH 05/83] heat solver interface --- CHANGELOG.md | 13 +- tests/sims/simulation_2_5_0rc1.json | 1969 +++++++++++++++++ tests/test_components/test_bc_placement.py | 20 + tests/test_components/test_heat.py | 364 +++ tests/test_components/test_scene.py | 274 +++ tests/test_components/test_simulation.py | 13 + tests/test_web/test_webapi_heat.py | 350 +++ tests/utils.py | 21 +- tidy3d/__init__.py | 38 + tidy3d/components/base.py | 2 + tidy3d/components/base_sim/__init__.py | 0 tidy3d/components/base_sim/data/__init__.py | 0 .../components/base_sim/data/monitor_data.py | 25 + tidy3d/components/base_sim/data/sim_data.py | 125 ++ tidy3d/components/base_sim/monitor.py | 233 ++ tidy3d/components/base_sim/simulation.py | 448 ++++ tidy3d/components/bc_placement.py | 117 + tidy3d/components/data/data_array.py | 6 +- tidy3d/components/data/sim_data.py | 2 +- tidy3d/components/geometry/base.py | 33 +- tidy3d/components/geometry/utils.py | 1 - tidy3d/components/heat/__init__.py | 0 tidy3d/components/heat/boundary.py | 93 + tidy3d/components/heat/data/__init__.py | 0 tidy3d/components/heat/data/monitor_data.py | 116 + tidy3d/components/heat/data/sim_data.py | 232 ++ tidy3d/components/heat/grid.py | 119 + tidy3d/components/heat/monitor.py | 27 + tidy3d/components/heat/simulation.py | 867 ++++++++ tidy3d/components/heat/source.py | 48 + tidy3d/components/heat/viz.py | 12 + tidy3d/components/heat_spec.py | 51 + tidy3d/components/medium.py | 29 + tidy3d/components/monitor.py | 4 +- tidy3d/components/scene.py | 1280 +++++++++++ tidy3d/components/simulation.py | 59 +- tidy3d/components/types.py | 3 + tidy3d/components/viz.py | 2 + tidy3d/constants.py | 6 + tidy3d/web/api/asynchronous.py | 10 +- tidy3d/web/api/container.py | 71 +- tidy3d/web/api/tidy3d_stub.py | 56 +- tidy3d/web/api/webapi.py | 264 ++- tidy3d/web/environment.py | 4 + 44 files changed, 7219 insertions(+), 188 deletions(-) create mode 100644 tests/sims/simulation_2_5_0rc1.json create mode 100644 tests/test_components/test_bc_placement.py create mode 100644 tests/test_components/test_heat.py create mode 100644 tests/test_components/test_scene.py create mode 100644 tests/test_web/test_webapi_heat.py create mode 100644 tidy3d/components/base_sim/__init__.py create mode 100644 tidy3d/components/base_sim/data/__init__.py create mode 100644 tidy3d/components/base_sim/data/monitor_data.py create mode 100644 tidy3d/components/base_sim/data/sim_data.py create mode 100644 tidy3d/components/base_sim/monitor.py create mode 100644 tidy3d/components/base_sim/simulation.py create mode 100644 tidy3d/components/bc_placement.py create mode 100644 tidy3d/components/heat/__init__.py create mode 100644 tidy3d/components/heat/boundary.py create mode 100644 tidy3d/components/heat/data/__init__.py create mode 100644 tidy3d/components/heat/data/monitor_data.py create mode 100644 tidy3d/components/heat/data/sim_data.py create mode 100644 tidy3d/components/heat/grid.py create mode 100644 tidy3d/components/heat/monitor.py create mode 100644 tidy3d/components/heat/simulation.py create mode 100644 tidy3d/components/heat/source.py create mode 100644 tidy3d/components/heat/viz.py create mode 100644 tidy3d/components/heat_spec.py create mode 100644 tidy3d/components/scene.py create mode 100644 tidy3d/web/environment.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 99cd6cd4a..c3d30ac9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,22 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 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 - Internal refactor of Web API functionality. - `Geometry.from_gds` doesn't create unecessary groups of single elements. +- Properly handle `.freqs` in `output_monitors` of adjoint plugin. ### Fixed -## [Unreleased] - -### Added - -### Changed - -### Fixed -- Properly handle `.freqs` in `output_monitors` of adjoint plugin. - ## [2.4.2] - 2023-9-28 ### Added diff --git a/tests/sims/simulation_2_5_0rc1.json b/tests/sims/simulation_2_5_0rc1.json new file mode 100644 index 000000000..c5875ea93 --- /dev/null +++ b/tests/sims/simulation_2_5_0rc1.json @@ -0,0 +1,1969 @@ +{ + "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, + "allow_gain": false, + "nonlinear_spec": null, + "heat_spec": 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": "dieletric_box", + "type": "Structure", + "medium": { + "name": "dieletric", + "frequency_range": null, + "allow_gain": false, + "nonlinear_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, + "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, + "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, + "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, + "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": [ + [ + 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, + "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, + "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", + "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, + "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, + "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 + }, + "yy": { + "name": null, + "frequency_range": null, + "allow_gain": false, + "nonlinear_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, + "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, + "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": [ + [ + { + "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, + "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 + } + } + ], + "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, + "remove_dc_component": true + }, + "name": null, + "interpolate": true, + "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, + "remove_dc_component": true + }, + "name": null, + "interpolate": true, + "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, + "remove_dc_component": true + }, + "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", + "group_index_step": false, + "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, + "remove_dc_component": true + }, + "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, + "remove_dc_component": true + }, + "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, + "remove_dc_component": true + }, + "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, + "remove_dc_component": true + }, + "name": null, + "field_dataset": { + "type": "FieldDataset", + "Ex": "ScalarFieldDataArray", + "Ey": null, + "Ez": null, + "Hx": null, + "Hy": null, + "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": [ + 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 + }, + "name": null, + "direction": "+", + "angle_theta": 0.5235987755982988, + "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": { + "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, + "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", + "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, + "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, + "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", + "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, + "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", + "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, + "heat_spec": null, + "type": "Medium", + "permittivity": 2.0, + "conductivity": 0.0 + } + } + ], + "type": "GridSpec" + }, + "shutoff": 0.0001, + "subpixel": false, + "normalize_index": 0, + "courant": 0.8, + "version": "2.5.0rc1" +} \ No newline at end of file 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_heat.py b/tests/test_components/test_heat.py new file mode 100644 index 000000000..d1792e233 --- /dev/null +++ b/tests/test_components/test_heat.py @@ -0,0 +1,364 @@ +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_mnt(): + return TemperatureMonitor(size=(1, 2, 3), name="test") + + +def test_heat_mnt(): + temp_mnt = make_heat_mnt() + + 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_mnt = make_heat_mnt() + + nx, ny, nz = 9, 6, 5 + x = np.linspace(-1, 1, nx) + y = np.linspace(-2, 2, ny) + z = np.linspace(-3, 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) + + return TemperatureData(monitor=temp_mnt, temperature=temperature_field) + + +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_mnt = make_heat_mnt() + + 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_mnt], + ) + + 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 = make_heat_mnt() + + 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) + plt.close() + + with pytest.raises(DataError): + _ = 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 = make_heat_mnt() + 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_scene.py b/tests/test_components/test_scene.py new file mode 100644 index 000000000..3c6c5939b --- /dev/null +++ b/tests/test_components/test_scene.py @@ -0,0 +1,274 @@ +"""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.simulation import MAX_NUM_MEDIUMS +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""" + + sim = 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), + ) + + _ = sim.mediums + _ = sim.medium_map + _ = sim.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_allow_gain(): + """Test if simulation allows gain.""" + + medium = td.Medium(permittivity=2.0) + medium_gain = td.Medium(permittivity=2.0, allow_gain=True) + medium_ani = td.AnisotropicMedium(xx=medium, yy=medium, zz=medium) + medium_gain_ani = td.AnisotropicMedium(xx=medium, yy=medium_gain, zz=medium) + + # Test simulation medium + scene = td.Scene(medium=medium) + assert not scene.allow_gain + scene = scene.updated_copy(medium=medium_gain) + assert scene.allow_gain + + # Test structure with anisotropic gain medium + struct = td.Structure(geometry=td.Box(center=(0, 0, 0), size=(1, 1, 1)), medium=medium_ani) + struct_gain = struct.updated_copy(medium=medium_gain_ani) + scene = td.Scene( + medium=medium, + structures=[struct], + ) + assert not scene.allow_gain + scene = scene.updated_copy(structures=[struct_gain]) + assert scene.allow_gain + + +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) diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 2dd5d2621..9b1b00b58 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -1962,3 +1962,16 @@ 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 diff --git a/tests/test_web/test_webapi_heat.py b/tests/test_web/test_webapi_heat.py new file mode 100644 index 000000000..22ee21e0b --- /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..496aba5e5 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -102,19 +102,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,7 +132,8 @@ ), 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)), @@ -135,6 +142,7 @@ 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 +151,7 @@ yy=td.Medium(permittivity=2), zz=td.Medium(permittivity=3), ), + name="anisotopic_cylinder", ), td.Structure( geometry=td.PolySlab( @@ -151,6 +160,7 @@ medium=td.PoleResidue( eps_inf=1.0, poles=((6206417594288582j, (-3.311074436985222e16j)),) ), + name="pole_slab", ), td.Structure( geometry=td.Box( @@ -231,6 +241,7 @@ ) ), medium=td.Medium(permittivity=5), + name="dieletric_mesh", ), td.Structure( geometry=td.TriangleMesh.from_stl( diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 5e30aa4a2..974a357fe 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -107,6 +107,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.""" @@ -265,4 +284,23 @@ 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", ] diff --git a/tidy3d/components/base.py b/tidy3d/components/base.py index 6db774667..a70c1e996 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -705,6 +705,8 @@ def __ge__(self, other): def __eq__(self, other): """Define == for two Tidy3DBaseModels.""" + if other is None: + return False return self._json_string == other._json_string @cached_property 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..3338d7378 --- /dev/null +++ b/tidy3d/components/base_sim/data/sim_data.py @@ -0,0 +1,125 @@ +"""Abstract base for simulation data structures.""" +from __future__ import annotations +from typing import Dict, Tuple + +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 ...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.""" + if monitor_name not in self.monitor_data: + raise DataError(f"'{self.type}' does not contain data for monitor '{monitor_name}'.") + 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: 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 diff --git a/tidy3d/components/base_sim/monitor.py b/tidy3d/components/base_sim/monitor.py new file mode 100644 index 000000000..65cc1ddc0 --- /dev/null +++ b/tidy3d/components/base_sim/monitor.py @@ -0,0 +1,233 @@ +"""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 Direction, Axis, BoxSurface +from ..geometry.base import Box +from ..validators import assert_plane +from ..base import cached_property +from ..viz import PlotParams, plot_params_monitor +from ...constants import SECOND +from ...exceptions import SetupError +from ...log import log + + +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))) + + +class AbstractTimeMonitor(AbstractMonitor, ABC): + """Abstract base class for transient monitors.""" + + start: pd.NonNegativeFloat = pd.Field( + 0.0, + title="Start time", + description="Time at which to start monitor recording.", + units=SECOND, + ) + + stop: pd.NonNegativeFloat = pd.Field( + None, + title="Stop time", + description="Time at which to stop monitor recording. " + "If not specified, record until end of simulation.", + units=SECOND, + ) + + interval: pd.PositiveInt = pd.Field( + 1, + title="Time interval", + description="Number of time step intervals between monitor recordings.", + ) + + @pd.validator("stop", always=True, allow_reuse=True) + def stop_greater_than_start(cls, val, values): + """Ensure sure stop is greater than or equal to start.""" + start = values.get("start") + if val and val < start: + raise SetupError("Monitor start time is greater than stop time.") + return val + + def time_inds(self, tmesh: ArrayFloat1D) -> Tuple[int, int]: + """Compute the starting and stopping index of the monitor in a given discrete time mesh.""" + + tmesh = np.array(tmesh) + tind_beg, tind_end = (0, 0) + + if tmesh.size == 0: + return (tind_beg, tind_end) + + # If monitor.stop is None, record until the end + t_stop = self.stop + if t_stop is None: + tind_end = int(tmesh.size) + t_stop = tmesh[-1] + else: + tend = np.nonzero(tmesh <= t_stop)[0] + if tend.size > 0: + tind_end = int(tend[-1] + 1) + + # Step to compare to in order to handle t_start = t_stop + dt = 1e-20 if np.array(tmesh).size < 2 else tmesh[1] - tmesh[0] + # If equal start and stopping time, record one time step + if np.abs(self.start - t_stop) < dt and self.start <= tmesh[-1]: + tind_beg = max(tind_end - 1, 0) + else: + tbeg = np.nonzero(tmesh[:tind_end] >= self.start)[0] + tind_beg = tbeg[0] if tbeg.size > 0 else tind_end + return (tind_beg, tind_end) + + def num_steps(self, tmesh: ArrayFloat1D) -> int: + """Compute number of time steps for a time monitor.""" + + tind_beg, tind_end = self.time_inds(tmesh) + return int((tind_end - tind_beg) / self.interval) + + +class AbstractPlanarMonitor(AbstractMonitor, ABC): + """:class:`AbstractMonitor` that has a planar geometry.""" + + _plane_validator = assert_plane() + + @cached_property + def normal_axis(self) -> Axis: + """Axis normal to the monitor's plane.""" + return self.size.index(0.0) + + +class AbstractSurfaceIntegrationMonitor(AbstractMonitor, ABC): + """Abstract class for monitors that perform surface integrals during the solver run.""" + + normal_dir: Direction = pd.Field( + None, + 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[BoxSurface, ...] = pd.Field( + None, + title="Excluded surfaces", + description="Surfaces to exclude in the integration, if a volume monitor.", + ) + + @property + def integration_surfaces(self): + """Surfaces of the monitor where fields will be recorded for subsequent integration.""" + if self.size.count(0.0) == 0: + return self.surfaces_with_exclusion(**self.dict()) + return [self] + + @pd.root_validator(skip_on_failure=True) + def normal_dir_exists_for_surface(cls, values): + """If the monitor is a surface, set default ``normal_dir`` if not provided. + If the monitor is a box, warn that ``normal_dir`` is relevant only for surfaces.""" + normal_dir = values.get("normal_dir") + name = values.get("name") + size = values.get("size") + if size.count(0.0) != 1: + if normal_dir is not None: + log.warning( + "The ``normal_dir`` field is relevant only for surface monitors " + f"and will be ignored for monitor {name}, which is a box." + ) + else: + if normal_dir is None: + values["normal_dir"] = "+" + return values + + @pd.root_validator(skip_on_failure=True) + def check_excluded_surfaces(cls, values): + """Error if ``exclude_surfaces`` is provided for a surface monitor.""" + exclude_surfaces = values.get("exclude_surfaces") + if exclude_surfaces is None: + return values + name = values.get("name") + size = values.get("size") + if size.count(0.0) > 0: + raise SetupError( + f"Can't specify ``exclude_surfaces`` for surface monitor {name}; " + "valid for box monitors only." + ) + return values diff --git a/tidy3d/components/base_sim/simulation.py b/tidy3d/components/base_sim/simulation.py new file mode 100644 index 000000000..a08b55efb --- /dev/null +++ b/tidy3d/components/base_sim/simulation.py @@ -0,0 +1,448 @@ +"""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 AbstractSurfaceIntegrationMonitor + +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, SetupError +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") + + _monitors_in_bounds = assert_objects_in_sim_bounds("monitors") + _structures_in_bounds = assert_objects_in_sim_bounds("structures", error=False) + + @pd.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.""" + + if val is None: + return val + + sim_center = values.get("center") + sim_size = values.get("size") + sim_box = Box(size=sim_size, center=sim_center) + + for mnt in (mnt for mnt in val if isinstance(mnt, AbstractSurfaceIntegrationMonitor)): + if not any(sim_box.intersects(surf) for surf in mnt.integration_surfaces): + raise SetupError( + f"All integration surfaces of monitor '{mnt.name}' are outside of the " + "simulation bounds." + ) + + return val + + @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." + ) + + return val + + """ 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): # -> Monitor: + """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.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.simulation_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.simulation_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. + """ + + @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/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..a1e9274c2 100644 --- a/tidy3d/components/data/data_array.py +++ b/tidy3d/components/data/data_array.py @@ -296,7 +296,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,7 +313,7 @@ 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]) ) diff --git a/tidy3d/components/data/sim_data.py b/tidy3d/components/data/sim_data.py index 98ad94a58..74f67eb73 100644 --- a/tidy3d/components/data/sim_data.py +++ b/tidy3d/components/data/sim_data.py @@ -195,7 +195,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 diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index c2a789821..49f4eccf4 100644 --- a/tidy3d/components/geometry/base.py +++ b/tidy3d/components/geometry/base.py @@ -358,10 +358,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: @@ -369,6 +381,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. diff --git a/tidy3d/components/geometry/utils.py b/tidy3d/components/geometry/utils.py index dbe9ad956..bd35299ed 100644 --- a/tidy3d/components/geometry/utils.py +++ b/tidy3d/components/geometry/utils.py @@ -80,7 +80,6 @@ def traverse_geometries(geometry: GeometryType) -> GeometryType: yield geometry -# pylint:disable=too-many-arguments def from_shapely( shape: Shapely, axis: Axis, 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..b14b4fb3d --- /dev/null +++ b/tidy3d/components/heat/data/monitor_data.py @@ -0,0 +1,116 @@ +"""Monitor level data, store the DataArrays associated with a single heat monitor.""" +from __future__ import annotations +from typing import Union, Tuple + +from abc import ABC + +import numpy as np +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 ...types import ScalarSymmetry, Coordinate +from ....constants import KELVIN + + +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 + >>> 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: SpatialDataArray = pd.Field( + ..., title="Temperature", description="Spatial temperature field.", units=KELVIN + ) + + @property + def symmetry_expanded_copy(self) -> TemperatureData: + """Return copy of self with symmetry applied.""" + + if all(sym == 0 for sym in self.symmetry): + return self.copy() + + coords = list(self.temperature.coords.values()) + data = np.array(self.temperature.data) + + for dim in range(3): + if self.symmetry[dim] == 1: + + sym_center = self.symmetry_center[dim] + + if sym_center == coords[dim].data[0]: + num_duplicates = 1 + else: + num_duplicates = 0 + + shape = np.array(np.shape(data)) + old_len = shape[dim] + shape[dim] = 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[dim] = slice(old_len - 1, None, -1) + ind_right[dim] = 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[dim]) + new_coords[old_len - num_duplicates :] = coords[dim] + new_coords[old_len - 1 :: -1] = 2 * sym_center - coords[dim] + + coords[dim] = new_coords + data = new_data + + coords_dict = dict(zip("xyz", coords)) + new_temperature = SpatialDataArray(data, coords=coords_dict) + + return self.updated_copy(temperature=new_temperature, 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..e1d909161 --- /dev/null +++ b/tidy3d/components/heat/data/sim_data.py @@ -0,0 +1,232 @@ +"""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 ...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', 'dB'] + Plot in linear or logarithmic (dB) 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'." + ) + + field_data = self._field_component_value(monitor_data.temperature, val) + + if scale == "log": + field_data = np.log10(np.abs(field_data)) + cmap_type = "sequential" + elif val == "real": + cmap_type = "divergent" + else: + cmap_type = "sequential" + + # 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]) + + # select the cross section data + interp_kwarg = {"xyz"[axis]: position} + + if cmap_type == "divergent": + cmap = "RdBu_r" + elif cmap_type == "sequential": + cmap = "magma" + + # plot the field + 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": "temperature"}, + ) + + # 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 + x_coord_values = field_data.coords[x_coord_label] + y_coord_values = field_data.coords[y_coord_label] + ax.set_xlim(min(x_coord_values), max(x_coord_values)) + ax.set_ylim(min(y_coord_values), max(y_coord_values)) + + 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..46e216707 --- /dev/null +++ b/tidy3d/components/heat/monitor.py @@ -0,0 +1,27 @@ +"""Objects that define how data is recorded from simulation.""" +from abc import ABC +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.""" + + 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..3fcb0bdcd --- /dev/null +++ b/tidy3d/components/heat/simulation.py @@ -0,0 +1,867 @@ +"""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 ..heat_spec import SolidSpec + +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 + + """ Post-init validators """ + + def _post_init_validators(self) -> None: + """Call validators taking z`self` that get run after init.""" + self._warn_multiple_zones() + + def _warn_multiple_zones(self): + """Warn about current restriction on number of adjacent zones.""" + + struc_src_map = {} + for source in self.sources: + for name in source.structures: + struc_src_map[name] = source + + unique_solid_zones = { + (struc.medium.heat_spec, struc_src_map.get(struc.name, None)) + for struc in self.structures + if isinstance(struc.medium.heat_spec, SolidSpec) + } + + if isinstance(self.medium.heat_spec, SolidSpec): + unique_solid_zones.add((self.medium.heat_spec, None)) + + if len(unique_solid_zones) > 2: + log.warning( + "More than 2 different solid zones (zone = medium + source) are detected in the heat " + "simulation. Make sure no more than 2 solid zones are adjacent to each other anywhere " + "in the simulation domain. The simulation results may be inaccurate otherwise. " + "This restriction will be removed in the upcoming Tidy3D versions." + ) + + @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..b72f6993a --- /dev/null +++ b/tidy3d/components/heat/source.py @@ -0,0 +1,48 @@ +"""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 Tidy3dBaseModel, cached_property +from ..data.data_array import TimeDataArray +from ..viz import PlotParams + +from ...constants import VOLUMETRIC_HEAT_RATE + + +class HeatSource(ABC, Tidy3dBaseModel): + """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..ac0c8c4da 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -26,6 +26,7 @@ from ..log import log from .transformation import RotationType from .parameter_perturbation import ParameterPerturbation +from .heat_spec import HeatSpecType # evaluate frequency as this number (Hz) if inf @@ -188,6 +189,20 @@ def _validate_nonlinear_spec(cls, val): f"currently supported for medium class {cls}." ) + 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, + ) + _name_validator = validate_name_str() @abstractmethod @@ -3224,6 +3239,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 +3254,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 +3315,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 +3330,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 ------- @@ -3342,6 +3365,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 +3431,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 +3446,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 +3482,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) diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 7b8a126f2..cfab76b6f 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -6,7 +6,7 @@ import numpy as np from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound, Numpy -from .types import Literal, Direction, Coordinate, Axis, ObsGridArray +from .types import Literal, Direction, Coordinate, Axis, ObsGridArray, BoxSurface from .geometry.base import Box from .validators import assert_plane from .base import cached_property, Tidy3dBaseModel @@ -496,7 +496,7 @@ class SurfaceIntegrationMonitor(Monitor, ABC): "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", description="Surfaces to exclude in the integration, if a volume monitor.", diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py new file mode 100644 index 000000000..ee167233f --- /dev/null +++ b/tidy3d/components/scene.py @@ -0,0 +1,1280 @@ +""" 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 +from .geometry.mesh import TriangleMesh +from .types import Ax, Shapely, TYPE_TAG_STR, Bound, Size, Coordinate, InterpMethod +from .medium import Medium, MediumType, PECMedium +from .medium import AbstractCustomMedium, Medium2D, MediumType3D +from .medium import AnisotropicMedium, AbstractPerturbationMedium +from .structure import Structure +from .data.dataset import Dataset +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 + + +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 + + """ 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_plane(structures=self.structures, x=x, y=y, z=z) + 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 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(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 + + @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 = 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, + ) -> 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 + structures = [self.background_structure] + list(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: + medium_shapes = self._get_structures_plane(structures=structures, x=x, y=y, z=z) + + 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 + ) + + 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 isinstance(medium, PECMedium)] + # regular medium + eps_list = [ + medium.eps_model(freq).real + for medium in medium_list + if not isinstance(medium, AbstractCustomMedium) + ] + eps_list.append(1) + eps_min = min(eps_list) + eps_max = max(eps_list) + # 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 = Box.parse_xyz_kwargs(x=x, y=y, z=z) + normal_axis, plane_axes = Box.pop_axis(coords, normal_axis_ind) + plane_axes_inds = [0, 1, 2] + plane_axes_inds.pop(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) + + # 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) + + # 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 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 + 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 + structures = [self.background_structure] + list(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: + medium_shapes = self._get_structures_plane(structures=structures, x=x, y=y, z=z) + + 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 """ + + @property + def custom_datasets(self) -> List[Dataset]: + """List of custom datasets for verification purposes. If the list is not empty, then + the scene needs to be exported to hdf5 to store the data. + """ + datasets_medium = [mat for mat in self.mediums if isinstance(mat, AbstractCustomMedium)] + datasets_geometry = [ + struct.geometry.mesh_dataset + for struct in self.structures + if isinstance(struct.geometry, TriangleMesh) + ] + return datasets_medium + datasets_geometry + + @cached_property + def allow_gain(self) -> bool: + """``True`` if any of the mediums in the scene allows gain.""" + + for medium in self.mediums: + if isinstance(medium, AnisotropicMedium): + if np.any([med.allow_gain for med in [medium.xx, medium.yy, medium.zz]]): + return True + elif medium.allow_gain: + return True + return False + + 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 0cb9c2335..186bc7bbe 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -19,7 +19,7 @@ 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, Shapely, FreqBound, Axis, annotate_type, Symmetry, TYPE_TAG_STR, InterpMethod from .grid.grid import Coords1D, Grid, Coords from .grid.grid_spec import GridSpec, UniformGrid, AutoGrid from .medium import Medium, MediumType, AbstractMedium, PECMedium @@ -37,6 +37,7 @@ 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 @@ -3169,6 +3170,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 @@ -3184,6 +3186,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 ------- @@ -3228,7 +3233,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: @@ -3254,6 +3259,54 @@ 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) + + @cached_property + def scene(self) -> Scene: + """Return a :class:.`Scene` instance based on the current simulation.""" + + return Scene( + structures=self.structures, + medium=self.medium, + ) + + @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/types.py b/tidy3d/components/types.py index 233d3ca4d..cf24e6cfa 100644 --- a/tidy3d/components/types.py +++ b/tidy3d/components/types.py @@ -178,6 +178,7 @@ def __modify_schema__(cls, field_schema): """ symmetry """ Symmetry = Literal[0, -1, 1] +ScalarSymmetry = Literal[0, 1] """ geometric """ @@ -192,6 +193,7 @@ 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+"] """ medium """ @@ -224,6 +226,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/viz.py b/tidy3d/components/viz.py index 89d2bcd55..43f979f73 100644 --- a/tidy3d/components/viz.py +++ b/tidy3d/components/viz.py @@ -114,6 +114,7 @@ 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") # stores color of simulation.structures for given index in simulation.medium_map MEDIUM_CMAP = [ @@ -129,6 +130,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..96429096e 100644 --- a/tidy3d/constants.py +++ b/tidy3d/constants.py @@ -48,6 +48,12 @@ CMCUBE = "cm^3" PERCMCUBE = "1/cm^3" +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/web/api/asynchronous.py b/tidy3d/web/api/asynchronous.py index 038c5b45a..249037dce 100644 --- a/tidy3d/web/api/asynchronous.py +++ b/tidy3d/web/api/asynchronous.py @@ -16,12 +16,12 @@ def run_async( simulation_type: str = "tidy3d", parent_tasks: Dict[str, List[str]] = None, ) -> BatchData: - """Submits a set of Union[: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, Union[: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,8 +38,8 @@ def run_async( Returns ------ :class:`BatchData` - Contains the Union[:class:`.SimulationData`] for each Union[: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/api/container.py b/tidy3d/web/api/container.py index 32142aec7..8d8c35596 100644 --- a/tidy3d/web/api/container.py +++ b/tidy3d/web/api/container.py @@ -27,10 +27,15 @@ class WebContainer(Tidy3dBaseModel, ABC): class Job(WebContainer): - """Interface for managing the running of Union[:class:`.Simulation`] on server.""" + """Interface for managing the running of Union[:class:`.Simulation`, :class:`.HeatSimulation`] + on server. + """ simulation: SimulationType = pd.Field( - ..., title="simulation", description="Simulation to run as a 'task'." + ..., + 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.") @@ -94,8 +99,9 @@ def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType: Returns ------- - Dict[str, Union[:class:`.SimulationData`]] - Dictionary mapping task name to Union[:class:`.SimulationData`] for :class:`Job`. + Dict[str, Union[:class:`.SimulationData`, :class:`.HeatSimulationData`]] + Dictionary mapping task name to + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for :class:`Job`. """ self.start() @@ -173,13 +179,14 @@ def download(self, path: str = DEFAULT_DATA_PATH) -> None: Note ---- - To load the data into Union[:class:`.SimulationData`] objets, can call :meth:`Job.load`. + To load the data into Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] objets, + can call :meth:`Job.load`. """ web.download(task_id=self.task_id, path=path, verbose=self.verbose) def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType: """Download results from simulation (if not already) and load them into - Union[:class:`.SimulationData`] object. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] object. Parameters ---------- @@ -188,7 +195,7 @@ def load(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType: Returns ------- - Union[:class:`.SimulationData`] + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] Object containing simulation results. """ return web.load(task_id=self.task_id, path=path, verbose=self.verbose) @@ -207,8 +214,8 @@ def estimate_cost(self) -> float: Returns ------- float - Estimated maximum cost for Union[:class:`.Simulation`] associated with - the given :class:`.Job`. + Estimated maximum cost for Union[:class:`.Simulation`, :class:`.HeatSimulation`] + associated with the given :class:`.Job`. Note ---- @@ -224,7 +231,9 @@ def estimate_cost(self) -> float: class BatchData(Tidy3dBaseModel): - """Holds a collection of Union[:class:`.SimulationData`] returned by :class:`.Batch`.""" + """Holds a collection of Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] returned + by :class:`.Batch`. + """ task_paths: Dict[TaskName, str] = pd.Field( ..., @@ -241,7 +250,9 @@ class BatchData(Tidy3dBaseModel): ) def load_sim_data(self, task_name: str) -> SimulationDataType: - """Load Union[:class:`.SimulationData`] from file by task name.""" + """Load Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] 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) @@ -253,12 +264,16 @@ def load_sim_data(self, task_name: str) -> SimulationDataType: ) def items(self) -> Tuple[TaskName, SimulationDataType]: - """Iterate through the Union[:class:`.SimulationData`] for each task_name.""" + """Iterate through the Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + 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) -> SimulationDataType: - """Get the Union[:class:`.SimulationData`] for a given ``task_name``.""" + """Get the Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for + a given ``task_name``. + """ return self.load_sim_data(task_name) @classmethod @@ -274,8 +289,8 @@ def load(cls, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: Returns ------ :class:`BatchData` - Contains Union[:class:`.SimulationData`] for each Union[: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) @@ -284,7 +299,9 @@ def load(cls, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: class Batch(WebContainer): - """Interface for submitting several Union[:class:`.Simulation`] objects to sever.""" + """Interface for submitting several Union[:class:`.Simulation`, :class:`.HeatSimulation`] + objects to sever. + """ simulations: Dict[TaskName, SimulationType] = pd.Field( ..., @@ -354,8 +371,8 @@ def run(self, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: Returns ------ :class:`BatchData` - Contains Union[:class:`.SimulationData`] of each Union[:class:`.Simulation`] - in :class:`Batch`. + Contains Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for + each Union[:class:`.Simulation`, :class:`.HeatSimulation`] in :class:`Batch`. Note ---- @@ -365,9 +382,10 @@ 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 Union[:class:`.SimulationData`] objects in memory, - rather it iterates over the task names - and loads the corresponding Union[:class:`.SimulationData`] from file one by one. + ``bach_data`` does not store all of + the Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] objects in memory, + rather it iterates over the task names and loads the corresponding + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] from file one by one. If no file exists for that task, it downloads it. """ self._check_path_dir(path_dir) @@ -572,7 +590,8 @@ def download(self, path_dir: str = DEFAULT_DATA_DIR) -> None: Note ---- - To load the data into Union[:class:`.SimulationData`] objets, can call :meth:`Batch.items`. + To load the data into Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] objets, + can call :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``, @@ -601,8 +620,8 @@ def load(self, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: Returns ------ :class:`BatchData` - Contains Union[:class:`.SimulationData`] of each Union[: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()``. @@ -645,8 +664,8 @@ def estimate_cost(self) -> float: Returns ------- float - Estimated maximum cost for each Union[:class:`.Simulation`] associated with - given :class:`.Batch`. + Estimated maximum cost for each Union[:class:`.Simulation`, :class:`.HeatSimulation`] + associated with given :class:`.Batch`. Note ---- diff --git a/tidy3d/web/api/tidy3d_stub.py b/tidy3d/web/api/tidy3d_stub.py index 2f2d4008c..54d6922e8 100644 --- a/tidy3d/web/api/tidy3d_stub.py +++ b/tidy3d/web/api/tidy3d_stub.py @@ -4,6 +4,7 @@ import json from typing import Union, Callable, List +import pydantic.v1 as pd from pydantic.v1 import BaseModel from ..core.file_util import ( @@ -19,29 +20,31 @@ 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] -SimulationDataType = Union[SimulationData] +SimulationType = Union[Simulation, HeatSimulation] +SimulationDataType = Union[SimulationData, HeatSimulationData] class Tidy3dStub(BaseModel, TaskStub): - simulation: SimulationType + simulation: SimulationType = pd.Field(discriminator="type") @classmethod def from_file(cls, file_path: str) -> SimulationType: - """Loads a Union[:class:`.Simulation`] from .yaml, .json, or .hdf5 file. + """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`] from. + Union[:class:`.Simulation`, :class:`.HeatSimulation`] from. Returns ------- - Union[:class:`.Simulation`] + Union[:class:`.Simulation`, :class:`.HeatSimulation`] An instance of the component class calling `load`. Example @@ -62,6 +65,8 @@ def from_file(cls, file_path: str) -> SimulationType: 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 @@ -69,7 +74,8 @@ def to_file( self, file_path: str, ): - """Exports Union[:class:`.Simulation`] instance to .yaml, .json, or .hdf5 file + """Exports Union[:class:`.Simulation`, :class:`.HeatSimulation`] instance to .yaml, .json, + or .hdf5 file Parameters ---------- @@ -83,12 +89,13 @@ def to_file( self.simulation.to_file(file_path) def to_hdf5_gz(self, fname: str, custom_encoders: List[Callable] = None) -> None: - """Exports Union[:class:`.Simulation`] instance to .hdf5.gz file. + """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`] to. + 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``. @@ -112,6 +119,8 @@ def get_type(self) -> str: 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""" @@ -126,17 +135,18 @@ class Tidy3dStubData(BaseModel, TaskStubData): @classmethod def from_file(cls, file_path: str) -> SimulationDataType: - """Loads a Union[:class:`.SimulationData`] from .yaml, .json, or .hdf5 file. + """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`] from. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] from. Returns ------- - Union[:class:`.SimulationData`] + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] An instance of the component class calling `load`. """ extension = _get_valid_extension(file_path) @@ -153,17 +163,20 @@ def from_file(cls, file_path: str) -> SimulationDataType: 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`] instance to .yaml, .json, or .hdf5 file + """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`] to. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] to. Example ------- @@ -173,17 +186,18 @@ def to_file(self, file_path: str): @classmethod def postprocess(cls, file_path: str) -> SimulationDataType: - """Load .yaml, .json, or .hdf5 file to Union[:class:`.SimulationData`] instance. + """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`] to. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] to. Returns ------- - Union[:class:`.SimulationData`] + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] An instance of the component class calling `load`. """ stub_data = Tidy3dStubData.from_file(file_path) @@ -193,8 +207,8 @@ def postprocess(cls, file_path: str) -> SimulationDataType: 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} " - f"is greater than the simulation shutoff threshold of {shutoff_value}. " - "Consider simulation again with large run_time duration for more accurate results." + 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/api/webapi.py b/tidy3d/web/api/webapi.py index 28c1f3621..c952a7c4b 100644 --- a/tidy3d/web/api/webapi.py +++ b/tidy3d/web/api/webapi.py @@ -48,12 +48,12 @@ def run( solver_version: str = None, worker_group: str = None, ) -> SimulationDataType: - """Submits a Union[:class:`.Simulation`] to server, starts running, monitors progress, - downloads, and loads results as a corresponding Union[:class:`.SimulationData`] object. + """Submits a Union[:class:`.Simulation`, :class:`.HeatSimulation`] to server, starts running, monitors progress, + downloads, and loads results as a corresponding Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] object. Parameters ---------- - simulation : Union[:class:`.Simulation`] + simulation : Union[:class:`.Simulation`, :class:`.HeatSimulation`] Simulation to upload to server. task_name : str Name of task. @@ -77,7 +77,7 @@ def run( Returns ------- - Union[:class:`.SimulationData`] + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] Object containing solver results for the supplied simulation. """ task_id = upload( @@ -111,11 +111,11 @@ def upload( parent_tasks: List[str] = None, source_required: bool = True, ) -> TaskId: - """Upload simulation to server, but do not start running Union[:class:`.Simulation`]. + """Upload simulation to server, but do not start running Union[:class:`.Simulation`, :class:`.HeatSimulation`]. Parameters ---------- - simulation : Union[:class:`.Simulation`] + simulation : Union[:class:`.Simulation`, :class:`.HeatSimulation`] Simulation to upload to server. task_name : str Name of task. @@ -158,8 +158,14 @@ def upload( console.log( f"Created task '{task_name}' with task_id '{task.task_id}' and task_type '{task_type}'." ) - url = _get_url(task.task_id) - console.log(f"View task using web UI at [link={url}]'{url}'[/link].") + 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) @@ -273,125 +279,159 @@ def monitor(task_id: TaskId, verbose: bool = True) -> None: ---- To load results when finished, may call :meth:`load`. """ + + console = get_logging_console() if verbose else None + task_info = get_info(task_id) - task_name = task_info.taskName - break_statuses = ("success", "error", "diverged", "deleted", "draft", "abort") + if task_info.taskType in ("MODE_SOLVER", "HEAT"): - console = get_logging_console() if verbose else None + log_level = "DEBUG" if verbose else "INFO" + solver_name = "Mode" if task_info.taskType == "MODE_SOLVER" else "Heat" - 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"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: + # 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("Error running mode 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.") + 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) - - 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 @@ -467,7 +507,7 @@ def download_hdf5( 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 Union[:class:`.Simulation`]. + """Download the `.json` file of a task and load the associated Union[:class:`.Simulation`, :class:`.HeatSimulation`]. Parameters ---------- @@ -480,8 +520,8 @@ def load_simulation( Returns ------- - Union[:class:`.Simulation`] - SimulationType loaded from downloaded json file. + Union[:class:`.Simulation`, :class:`.HeatSimulation`] + Simulation loaded from downloaded json file. """ task = SimulationTask.get(task_id) @@ -525,7 +565,7 @@ def load( verbose: bool = True, progress_callback: Callable[[float], None] = None, ) -> SimulationDataType: - """Download and Load simulation results into Union[:class:`.SimulationData`] object. + """Download and Load simulation results into Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] object. Parameters ---------- @@ -542,7 +582,7 @@ def load( Returns ------- - Union[:class:`.SimulationData`] + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] Object containing simulation data. """ if not os.path.exists(path) or replace_existing: diff --git a/tidy3d/web/environment.py b/tidy3d/web/environment.py new file mode 100644 index 000000000..a1643b093 --- /dev/null +++ b/tidy3d/web/environment.py @@ -0,0 +1,4 @@ +""" preserve from tidy3d.web.environment import Env backward compatibility """ +from .core.environment import Env + +__all__ = ["Env"] From e7ec44e92419b4e02444d24209384f75f3f1cc91 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Fri, 6 Oct 2023 13:34:21 -0400 Subject: [PATCH 06/83] add divergence mention to PML warning --- tidy3d/components/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 186bc7bbe..6eeb7652b 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -543,7 +543,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], ) From b473e667ff76cef68334b5cce0e15b0810a56c65 Mon Sep 17 00:00:00 2001 From: Lucas <119979961+lucas-flexcompute@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:58:32 -0300 Subject: [PATCH 07/83] Issue a warning when accessing unavailable group index data (#1169) (#1178) Signed-off-by: Lucas Heitzmann Gabrielli --- tests/test_plugins/test_mode_solver.py | 7 ++++++- tidy3d/components/data/dataset.py | 14 +++++++++++++- tidy3d/components/data/monitor_data.py | 18 +++++++++++++++--- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 2cd9f6c66..c3b04091a 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -531,7 +531,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( @@ -570,6 +570,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( diff --git a/tidy3d/components/data/dataset.py b/tidy3d/components/data/dataset.py index b5f4fa418..81a0ff5fa 100644 --- a/tidy3d/components/data/dataset.py +++ b/tidy3d/components/data/dataset.py @@ -329,8 +329,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 +352,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( diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index b30cf4250..eb0b0a95a 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -1179,7 +1179,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 +1313,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 +1394,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 +1411,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] From a9706bc282ee68e1286cac98b4fa8764959d7bff Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Tue, 10 Oct 2023 13:02:29 -0500 Subject: [PATCH 08/83] prep for 2.5.0rc1 --- CHANGELOG.md | 11 +- tidy3d/schema.json | 466 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 448 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d30ac9f..a649562e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +### Changed + +### Fixed + +## [2.5.0rc1] - 2023-10-10 + ### Added - 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. @@ -972,7 +980,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.2...develop +[Unreleased]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc1...pre/2.5 +[2.5.0rc1]: https://github.com/flexcompute/tidy3d/compare/v2.4.2...v2.5.0rc1 [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 [2.4.0]: https://github.com/flexcompute/tidy3d/compare/v2.3.3...v2.4.0 diff --git a/tidy3d/schema.json b/tidy3d/schema.json index ba94a9116..82b51abfa 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.2\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.\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, heat_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.5.0rc1\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... )", "type": "object", "properties": { "type": { @@ -72,6 +72,7 @@ "frequency_range": null, "allow_gain": false, "nonlinear_spec": null, + "heat_spec": null, "type": "Medium", "permittivity": 1.0, "conductivity": 0.0 @@ -508,7 +509,7 @@ "version": { "title": "Version", "description": "String specifying the front end version number.", - "default": "2.4.2", + "default": "2.5.0rc1", "type": "string" } }, @@ -550,9 +551,59 @@ ], "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 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.\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": { @@ -594,6 +645,25 @@ } ] }, + "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", @@ -641,7 +711,7 @@ }, "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 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.\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": { @@ -683,6 +753,25 @@ } ] }, + "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 +862,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 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.\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": { @@ -815,6 +904,25 @@ } ] }, + "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": "Sellmeier", @@ -854,7 +962,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 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.\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": { @@ -896,6 +1004,25 @@ } ] }, + "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 +1074,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 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.\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": { @@ -989,6 +1116,25 @@ } ] }, + "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 +1182,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 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.\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": { @@ -1078,6 +1224,25 @@ } ] }, + "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", @@ -1125,7 +1290,7 @@ }, "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 : Optional[NonlinearSusceptibility] = None\n Nonlinear 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]\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)", "type": "object", "properties": { "name": { @@ -1166,6 +1331,25 @@ } ] }, + "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", @@ -1289,7 +1473,7 @@ }, "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.", + "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.\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": { @@ -1331,6 +1515,25 @@ } ] }, + "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", @@ -1344,7 +1547,7 @@ }, "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)", + "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.\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": { @@ -1386,6 +1589,25 @@ } ] }, + "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": "FullyAnisotropicMedium", @@ -1508,7 +1730,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 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.\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.\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)", "type": "object", "properties": { "name": { @@ -1550,6 +1772,25 @@ } ] }, + "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": "CustomMedium", @@ -1618,7 +1859,7 @@ }, "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 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.\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": { @@ -1660,6 +1901,25 @@ } ] }, + "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 +2010,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 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.\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": { @@ -1792,6 +2052,25 @@ } ] }, + "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 +2145,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 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.\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": { @@ -1908,6 +2187,25 @@ } ] }, + "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 +2310,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 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.\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": { @@ -2054,6 +2352,25 @@ } ] }, + "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 +2461,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 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.\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": { @@ -2186,6 +2503,25 @@ } ] }, + "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 +2612,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 : Optional[NonlinearSusceptibility] = None\n Nonlinear 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": { @@ -2317,6 +2653,25 @@ } ] }, + "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 +3172,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 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.\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": { @@ -2873,6 +3228,25 @@ } ] }, + "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 +3287,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 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.\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": { @@ -2969,6 +3343,25 @@ } ] }, + "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`).", @@ -3666,7 +4059,7 @@ }, "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 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.\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]\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)", "type": "object", "properties": { "name": { @@ -3708,6 +4101,25 @@ } ] }, + "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", @@ -4277,7 +4689,7 @@ }, "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----------\ncenter : Tuple[float, float, float] = (0.0, 0.0, 0.0)\n [units = um]. Center of object in x, y, and z.\nsize : Tuple[Literal[0], Literal[0], 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')", "type": "object", "properties": { "type": { @@ -6398,7 +6810,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 : PositiveInt = 1\n Number of time step intervals between monitor recordings.\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 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')", "type": "object", "properties": { "type": { @@ -6509,8 +6921,7 @@ }, "interval": { "title": "Time interval", - "description": "Number of time step intervals between monitor recordings.", - "default": 1, + "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" }, @@ -6856,7 +7267,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 : PositiveInt = 1\n Number of time step intervals between monitor recordings.\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 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')", "type": "object", "properties": { "type": { @@ -6977,8 +7388,7 @@ }, "interval": { "title": "Time interval", - "description": "Number of time step intervals between monitor recordings.", - "default": 1, + "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" }, From 1f655c239733083d82c7a5fdae7ec971a0477bfc Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Tue, 26 Sep 2023 11:14:35 -0400 Subject: [PATCH 09/83] broadband adjoint --- CHANGELOG.md | 3 +- tests/test_plugins/test_adjoint.py | 110 +++++++------ tidy3d/components/source.py | 4 + .../adjoint/components/data/monitor_data.py | 141 ++++++++-------- .../adjoint/components/data/sim_data.py | 41 ++++- tidy3d/plugins/adjoint/components/geometry.py | 24 ++- tidy3d/plugins/adjoint/components/medium.py | 46 ++++-- .../plugins/adjoint/components/simulation.py | 151 ++++++++++++------ .../plugins/adjoint/components/structure.py | 14 +- tidy3d/plugins/adjoint/web.py | 21 ++- 10 files changed, 352 insertions(+), 203 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a649562e1..354bf38a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Support for multiple frequencies in `output_monitors` in `adjoint` plugin. ### Changed @@ -22,9 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Internal refactor of Web API functionality. - `Geometry.from_gds` doesn't create unecessary groups of single elements. -- Properly handle `.freqs` in `output_monitors` of adjoint plugin. ### Fixed +- Properly handle `.freqs` in `output_monitors` of adjoint plugin. ## [2.4.2] - 2023-9-28 diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index 5ebfb57fb..9fd7d0137 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -21,7 +21,7 @@ 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.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 @@ -54,6 +54,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, @@ -255,7 +261,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 +282,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", ) @@ -301,6 +307,7 @@ def make_sim( jax_struct_custom_anis, ), 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 +557,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 +567,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 +593,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 +618,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 +632,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 +644,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 +687,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 @@ -1548,3 +1544,27 @@ 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 diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index e7868fe35..e94eed959 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -98,6 +98,10 @@ def spectrum( if not complex_fields: time_amps = np.real(time_amps) + # if all time amplitudes are zero, just return (complex-valued) zeros for spectrum + if np.allclose(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 diff --git a/tidy3d/plugins/adjoint/components/data/monitor_data.py b/tidy3d/plugins/adjoint/components/data/monitor_data.py index ab1c6bc48..eb620c9a4 100644 --- a/tidy3d/plugins/adjoint/components/data/monitor_data.py +++ b/tidy3d/plugins/adjoint/components/data/monitor_data.py @@ -232,90 +232,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: + + # Define source geometry based on coordinates in the data + data_mins = [] + data_maxs = [] - adj_amp = scaling_factor * forward_amp + 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 - src_adj = PointDipole( - center=self.monitor.center, - polarization=polarization, + 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..a4b28304a 100644 --- a/tidy3d/plugins/adjoint/components/data/sim_data.py +++ b/tidy3d/plugins/adjoint/components/data/sim_data.py @@ -4,6 +4,8 @@ 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 @@ -157,7 +159,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 +173,19 @@ 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=()) + 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 +198,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..3512270bd 100644 --- a/tidy3d/plugins/adjoint/components/medium.py +++ b/tidy3d/plugins/adjoint/components/medium.py @@ -191,10 +191,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")) - freq = d_eps_map.coords["f"][0] - vjp_eps, vjp_sigma = self.eps_complex_to_eps_sigma(vjp_eps_complex, freq) + vjp_eps = 0.0 + vjp_sigma = 0.0 + + 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 +280,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 +527,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..08213bc40 100644 --- a/tidy3d/plugins/adjoint/components/simulation.py +++ b/tidy3d/plugins/adjoint/components/simulation.py @@ -17,7 +17,7 @@ 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 @@ -25,9 +25,15 @@ 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 @@ -69,6 +75,13 @@ 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, + ) + @register_pytree_node_class class JaxSimulation(Simulation, JaxObject): @@ -105,33 +118,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): @@ -227,18 +225,38 @@ def _warn_if_colocate(cls, val): 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 _is_multi_freq(self) -> bool: + """Does this simulation have a multi-frequency output?""" + return len(self.freqs_adjoint) > 1 @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 _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 +266,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 - # if no sources, just use a constant factor times the adjoint frequency + # 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 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 +325,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 +339,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 +348,7 @@ 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, ) return sim, jax_info @@ -522,7 +574,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 +596,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 +626,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 +650,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..065cfcb09 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 + import pydantic.v1 as pd import numpy as np from jax.tree_util import register_pytree_node_class @@ -74,10 +76,10 @@ def store_vjp( ) -> JaxStructure: """Returns the gradient of the structure parameters given forward and adjoint field data.""" - # 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) + # compute minimum wavelength in material (to use for determining integration points) + freq_max = max(grad_data_eps.eps_xx.f) + wvl_free_space = C_0 / freq_max + eps_in = self.medium.eps_model(frequency=freq_max) ref_ind = np.sqrt(np.max(np.real(eps_in))) wvl_mat = wvl_free_space / ref_ind @@ -102,6 +104,6 @@ def store_vjp( return self.copy(update=dict(geometry=geo_vjp, medium=medium_vjp)) - def make_grad_monitors(self, freq: float, name: str) -> FieldMonitor: + def make_grad_monitors(self, freqs: List[float], name: str) -> FieldMonitor: """Return gradient monitor associated with this object.""" - return self.geometry.make_grad_monitors(freq=freq, name=name) + return self.geometry.make_grad_monitors(freqs=freqs, name=name) diff --git a/tidy3d/plugins/adjoint/web.py b/tidy3d/plugins/adjoint/web.py index 23c97e6f4..87dd5401b 100644 --- a/tidy3d/plugins/adjoint/web.py +++ b/tidy3d/plugins/adjoint/web.py @@ -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 From eea873aee47edc8960e7af594304cdd719b8e446 Mon Sep 17 00:00:00 2001 From: Lucas <119979961+lucas-flexcompute@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:31:50 -0300 Subject: [PATCH 10/83] Add `to_gds` to `Simulation`, `Structure` and `Geometry` (#883) (#1194) Structures with custom medium are intersected with their medium contours at a given permittivity threshold. Signed-off-by: Lucas Heitzmann Gabrielli --- CHANGELOG.md | 1 + tests/test_components/test_geometry.py | 17 +- tests/test_components/test_simulation.py | 78 ++++++- tests/test_components/test_structure.py | 73 ++++++ tidy3d/components/geometry/base.py | 240 +++++++++++++++++++- tidy3d/components/geometry/utils.py | 26 ++- tidy3d/components/simulation.py | 276 ++++++++++++++++++++++- tidy3d/components/structure.py | 236 ++++++++++++++++++- 8 files changed, 926 insertions(+), 21 deletions(-) create mode 100644 tests/test_components/test_structure.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 354bf38a5..199e83ab0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for multiple frequencies in `output_monitors` in `adjoint` plugin. +- GDSII export functions to `Simulation`, `Structure`, and `Geometry`. ### Changed diff --git a/tests/test_components/test_geometry.py b/tests/test_components/test_geometry.py index 04330a9ff..44b67f0d6 100644 --- a/tests/test_components/test_geometry.py +++ b/tests/test_components/test_geometry.py @@ -258,7 +258,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) @@ -692,6 +692,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_simulation.py b/tests/test_components/test_simulation.py index 9b1b00b58..d591f6f81 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -2,6 +2,7 @@ import pytest import pydantic.v1 as pydantic import matplotlib.pyplot as plt +import gdstk import numpy as np import tidy3d as td @@ -85,12 +86,12 @@ def test_sim_init(): # 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) @@ -574,7 +575,7 @@ 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() @@ -1975,3 +1976,70 @@ def test_scene_from_scene(): ) 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=[1, 2], 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_structure.py b/tests/test_components/test_structure.py new file mode 100644 index 000000000..6381e1ffd --- /dev/null +++ b/tests/test_components/test_structure.py @@ -0,0 +1,73 @@ +import gdstk +import numpy as np +import tidy3d as td + + +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_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) diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index 49f4eccf4..285f8f7aa 100644 --- a/tidy3d/components/geometry/base.py +++ b/tidy3d/components/geometry/base.py @@ -20,9 +20,28 @@ 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, + 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 @@ -952,19 +971,21 @@ def from_gds( 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 = [] @@ -1023,6 +1044,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): @@ -2134,4 +2350,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/utils.py b/tidy3d/components/geometry/utils.py index bd35299ed..9a1c8d005 100644 --- a/tidy3d/components/geometry/utils.py +++ b/tidy3d/components/geometry/utils.py @@ -2,7 +2,7 @@ from __future__ import annotations from typing import Union, Tuple -from ..types import Axis, PlanePosition, Shapely +from ..types import Axis, PlanePosition, Shapely, ArrayFloat2D from ...exceptions import Tidy3dError from . import base @@ -157,3 +157,27 @@ 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.") diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 6eeb7652b..2e9a8578b 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -45,10 +45,22 @@ from ..version import __version__ from ..constants import C_0, SECOND, inf, fp_eps -from ..exceptions import Tidy3dKeyError, SetupError, ValidationError, Tidy3dError +from ..exceptions import Tidy3dKeyError, SetupError, ValidationError, Tidy3dError, Tidy3dImportError from ..log import log from ..updater import Updater +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 @@ -1383,6 +1395,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 diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index accbdfaea..f1a676c55 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -1,6 +1,7 @@ """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 @@ -10,7 +11,19 @@ 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): @@ -138,6 +151,227 @@ 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) + + eps, _, _ = self.medium.eps_dataarray_freq(frequency=frequency) + scale = min(np.diff(eps.x).min(), np.diff(eps.y).min(), np.diff(eps.z).min()) + + bb_min, bb_max = self.geometry.bounds + 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. From 8ba36e4c26ec2eddca3880ebbc8246843e83ce8a Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Thu, 12 Oct 2023 13:09:19 -0400 Subject: [PATCH 11/83] bump version to 2.5.0rc2 --- tidy3d/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/version.py b/tidy3d/version.py index 658327077..cc231c249 100644 --- a/tidy3d/version.py +++ b/tidy3d/version.py @@ -1,3 +1,3 @@ """Defines the front end version of tidy3d""" -__version__ = "2.5.0rc1" +__version__ = "2.5.0rc2" From 7cef26dcf1b3556d0039fa52eea44ae978a2812f Mon Sep 17 00:00:00 2001 From: qingeng Date: Mon, 16 Oct 2023 20:47:27 +0800 Subject: [PATCH 12/83] Optimize workbench url. --- tidy3d/web/api/webapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/web/api/webapi.py b/tidy3d/web/api/webapi.py index c952a7c4b..392c74913 100644 --- a/tidy3d/web/api/webapi.py +++ b/tidy3d/web/api/webapi.py @@ -32,7 +32,7 @@ def _get_url(task_id: str) -> str: """Get the URL for a task on our server.""" - return f"https://tidy3d.simulation.cloud/workbench?taskId={task_id}" + return f"{Env.current.website_endpoint}/workbench?taskId={task_id}" @wait_for_connection From cb9af20b056b98777221fa4cb68438a1c13d69b8 Mon Sep 17 00:00:00 2001 From: momchil Date: Mon, 16 Oct 2023 16:37:56 -0700 Subject: [PATCH 13/83] Adding verbose argument to estimate_cost and real_cost more detailed messages cleaning up some docstrings --- CHANGELOG.md | 1 + tidy3d/web/api/container.py | 139 +++++++++++++++++++---------------- tidy3d/web/api/webapi.py | 53 ++++++++++--- tidy3d/web/core/task_info.py | 1 + 4 files changed, 122 insertions(+), 72 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 199e83ab0..b99260ed5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support for multiple frequencies in `output_monitors` in `adjoint` plugin. - GDSII export functions 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. ### Changed diff --git a/tidy3d/web/api/container.py b/tidy3d/web/api/container.py index 8d8c35596..df1da2340 100644 --- a/tidy3d/web/api/container.py +++ b/tidy3d/web/api/container.py @@ -27,9 +27,7 @@ class WebContainer(Tidy3dBaseModel, ABC): class Job(WebContainer): - """Interface for managing the running of Union[:class:`.Simulation`, :class:`.HeatSimulation`] - on server. - """ + """Interface for managing the task runs on the server.""" simulation: SimulationType = pd.Field( ..., @@ -99,9 +97,8 @@ def run(self, path: str = DEFAULT_DATA_PATH) -> SimulationDataType: Returns ------- - Dict[str, Union[:class:`.SimulationData`, :class:`.HeatSimulationData`]] - Dictionary mapping task name to - Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for :class:`Job`. + Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] + Object containing simulation results. """ self.start() @@ -179,14 +176,12 @@ def download(self, path: str = DEFAULT_DATA_PATH) -> None: Note ---- - To load the data into Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] 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) -> SimulationDataType: - """Download results from simulation (if not already) and load them into - Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] object. + """Download job results and load them into a data object. Parameters ---------- @@ -204,36 +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 Union[:class:`.Simulation`, :class:`.HeatSimulation`] - associated with the given :class:`.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 Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] returned - by :class:`.Batch`. - """ + """Holds a collection of data objects returned by :class:`.Batch`.""" task_paths: Dict[TaskName, str] = pd.Field( ..., @@ -250,9 +253,7 @@ class BatchData(Tidy3dBaseModel): ) def load_sim_data(self, task_name: str) -> SimulationDataType: - """Load Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] from file - by task name. - """ + """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) @@ -264,16 +265,12 @@ def load_sim_data(self, task_name: str) -> SimulationDataType: ) def items(self) -> Tuple[TaskName, SimulationDataType]: - """Iterate through the Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] - for each task_name. - """ + """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) -> SimulationDataType: - """Get the Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] for - a given ``task_name``. - """ + """Get the simulation data object for a given ``task_name``.""" return self.load_sim_data(task_name) @classmethod @@ -299,9 +296,7 @@ def load(cls, path_dir: str = DEFAULT_DATA_DIR) -> BatchData: class Batch(WebContainer): - """Interface for submitting several Union[:class:`.Simulation`, :class:`.HeatSimulation`] - objects to sever. - """ + """Interface for submitting multiple simulations to the sever.""" simulations: Dict[TaskName, SimulationType] = pd.Field( ..., @@ -382,11 +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 Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] objects in memory, + ``bach_data`` does not store all of the data objects in memory, rather it iterates over the task names and loads the corresponding - Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] from file one by one. - If no file exists for that task, it downloads it. + data from file one by one. If no file exists for that task, it downloads it. """ self._check_path_dir(path_dir) self.start() @@ -494,13 +487,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 @@ -590,8 +581,7 @@ def download(self, path_dir: str = DEFAULT_DATA_DIR) -> None: Note ---- - To load the data into Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] 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``, @@ -649,32 +639,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 Union[:class:`.Simulation`, :class:`.HeatSimulation`] - associated with given :class:`.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/api/webapi.py b/tidy3d/web/api/webapi.py index 392c74913..26b4b0cf2 100644 --- a/tidy3d/web/api/webapi.py +++ b/tidy3d/web/api/webapi.py @@ -48,8 +48,8 @@ def run( solver_version: str = None, worker_group: str = None, ) -> SimulationDataType: - """Submits a Union[:class:`.Simulation`, :class:`.HeatSimulation`] to server, starts running, monitors progress, - downloads, and loads results as a corresponding Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] object. + """Submits a simulation to the server, starts running, monitors progress, + downloads, and loads results as a corresponding data object. Parameters ---------- @@ -111,7 +111,7 @@ def upload( parent_tasks: List[str] = None, source_required: bool = True, ) -> TaskId: - """Upload simulation to server, but do not start running Union[:class:`.Simulation`, :class:`.HeatSimulation`]. + """Upload a simulation to the server, but do not start running. Parameters ---------- @@ -302,7 +302,7 @@ def monitor(task_id: TaskId, verbose: bool = True) -> None: status = get_status(task_id) if status == "error": - raise WebError("Error running mode solver.") + raise WebError(f"Error running {solver_name} solver.") log.log(log_level, f"{solver_name} solver status: {status}") if verbose: @@ -402,7 +402,7 @@ def monitor_preprocess() -> None: 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.") + console.log(f"early shutoff detected at {perc_done:1.0f}%, exiting.") new_description = f"solver progress (field decay = {field_decay:.2e})" progress.update(pbar_pd, completed=100, refresh=True, description=new_description) @@ -507,7 +507,7 @@ def download_hdf5( 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 Union[:class:`.Simulation`, :class:`.HeatSimulation`]. + """Download the `.json` file of a task and load the associated simulation. Parameters ---------- @@ -565,7 +565,7 @@ def load( verbose: bool = True, progress_callback: Callable[[float], None] = None, ) -> SimulationDataType: - """Download and Load simulation results into Union[:class:`.SimulationData`, :class:`.HeatSimulationData`] object. + """Download and load simulation results into a data object. Parameters ---------- @@ -706,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 ---------- @@ -750,6 +753,13 @@ 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." + ) return task_info.estFlexUnit log.warning( @@ -760,9 +770,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``, @@ -770,11 +792,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/core/task_info.py b/tidy3d/web/core/task_info.py index 2684ae9d2..a8a74f336 100644 --- a/tidy3d/web/core/task_info.py +++ b/tidy3d/web/core/task_info.py @@ -60,6 +60,7 @@ class TaskInfo(TaskBase): estCostMin: float = None estCostMax: float = None realFlexUnit: float = None + oriRealFlexUnit: float = None estFlexUnit: float = None s3Storage: float = None startSolverTime: Optional[datetime] = None From a813f9dcaaf7a0632d21d007bed0c4f2775b4231 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Wed, 18 Oct 2023 16:02:45 -0400 Subject: [PATCH 14/83] unpin boto and click --- CHANGELOG.md | 1 + requirements/web.txt | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b99260ed5..aee37c4c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - ``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. ### Changed +- Update versions of `boto3`, `requests`, and `click`. ### Fixed 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 From aefc12d47036005126d00c8141a9ad353f49c8d4 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 2 Oct 2023 09:40:46 -0400 Subject: [PATCH 15/83] few changes for 2.5 --- CHANGELOG.md | 2 ++ tests/test_components/test_monitor.py | 2 +- tidy3d/components/monitor.py | 19 +++---------------- tox.ini | 2 -- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aee37c4c5..a360af4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Internal refactor of Web API functionality. - `Geometry.from_gds` doesn't create unecessary groups of single elements. +- python 3.7 no longer tested. +- All monitors now have `colocate=True` by default. ### Fixed - Properly handle `.freqs` in `output_monitors` of adjoint plugin. diff --git a/tests/test_components/test_monitor.py b/tests/test_components/test_monitor.py index f04171eba..2afc3d432 100644 --- a/tests/test_components/test_monitor.py +++ b/tests/test_components/test_monitor.py @@ -235,7 +235,7 @@ def test_monitor_colocate(log_capture): 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), diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index cfab76b6f..bd2a5ced2 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -290,25 +290,12 @@ class AbstractFieldMonitor(Monitor, ABC): ) colocate: bool = pydantic.Field( - None, + 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.""" @@ -653,7 +640,7 @@ class ModeSolverMonitor(AbstractModeMonitor): 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).", ) def storage_size(self, num_cells: int, tmesh: int) -> int: diff --git a/tox.ini b/tox.ini index d137603a0..a81312de8 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 From 81d7133d032b88d2d289a424ce7014619fa0ff63 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Fri, 20 Oct 2023 13:41:04 -0400 Subject: [PATCH 16/83] no longer support python3.7 --- .github/workflows/run_tests.yml | 5 +---- CHANGELOG.md | 4 ++-- setup.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) 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 a360af4d4..090a70326 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Internal refactor of Web API functionality. -- `Geometry.from_gds` doesn't create unecessary groups of single elements. -- python 3.7 no longer tested. +- `Geometry.from_gds` doesn't create unnecessary groups of single elements. +- python 3.7 no longer tested nor supported. - All monitors now have `colocate=True` by default. ### Fixed diff --git a/setup.py b/setup.py index c0ed27d21..82d5efc50 100644 --- a/setup.py +++ b/setup.py @@ -64,7 +64,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, From d02eb6cebf704456e669f26adc7397ae938fe7cd Mon Sep 17 00:00:00 2001 From: momchil Date: Fri, 20 Oct 2023 16:38:52 -0700 Subject: [PATCH 17/83] Fixing up messy changelog --- CHANGELOG.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e8d1808e0..68e655704 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [2.4.3] - 2023-10-16 - ### Added - Support for multiple frequencies in `output_monitors` in `adjoint` plugin. - GDSII export functions to `Simulation`, `Structure`, and `Geometry`. @@ -14,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update versions of `boto3`, `requests`, and `click`. +- python 3.7 no longer tested nor supported. +- Remove warning that monitors now have `colocate=True` by default. ### Fixed @@ -28,8 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Internal refactor of Web API functionality. - `Geometry.from_gds` doesn't create unnecessary groups of single elements. -- python 3.7 no longer tested nor supported. -- All monitors now have `colocate=True` by default. + +## [2.4.3] - 2023-10-16 + +### Added - `Geometry.zero_dims` method that uses `Geometry.bounds` and speeds up the validator for zero-sized geometries. ### Changed From 901ce1bdd14cf0010ffa74f987852febfe4cba04 Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Wed, 19 Jul 2023 17:35:03 -0700 Subject: [PATCH 18/83] Space-time modulation of materials with ModulationSpec Split the abstract time_dependence into time.py Validate types of materials that support time modulation --- CHANGELOG.md | 1 + tests/test_components/test_time_modulation.py | 205 +++++++++++ tidy3d/__init__.py | 8 + tidy3d/components/medium.py | 328 +++++++++++++++--- tidy3d/components/simulation.py | 11 +- tidy3d/components/source.py | 159 +-------- tidy3d/components/time.py | 206 +++++++++++ tidy3d/components/time_modulation.py | 247 +++++++++++++ 8 files changed, 971 insertions(+), 194 deletions(-) create mode 100644 tests/test_components/test_time_modulation.py create mode 100644 tidy3d/components/time.py create mode 100644 tidy3d/components/time_modulation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 68e655704..9fb5fa0bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for multiple frequencies in `output_monitors` in `adjoint` plugin. - GDSII export functions 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. +- Added 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. ### Changed - Update versions of `boto3`, `requests`, and `click`. 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/tidy3d/__init__.py b/tidy3d/__init__.py index 974a357fe..a229fe99a 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -23,6 +23,10 @@ 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 @@ -303,4 +307,8 @@ def set_logging_level(level: str) -> None: "DistanceUnstructuredGrid", "TemperatureData", "TemperatureMonitor", + "SpaceTimeModulation", + "SpaceModulation", + "ContinuousWaveTimeModulation", + "ModulationSpec", ] diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index ac0c8c4da..464db9a83 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -27,7 +27,7 @@ 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 @@ -167,7 +167,7 @@ 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.", @@ -179,6 +179,12 @@ class AbstractMedium(ABC, Tidy3dBaseModel): description="Nonlinear spec applied on top of the base medium properties.", ) + modulation_spec: ModulationSpec = pd.Field( + None, + title="Modulation Spec", + description="Modulation spec applied on top of the base medium properties.", + ) + @pd.validator("nonlinear_spec", always=True) def _validate_nonlinear_spec(cls, val): """Check compatibility with nonlinear_spec.""" @@ -203,8 +209,25 @@ def _validate_nonlinear_spec(cls, val): 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. @@ -338,7 +361,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 @@ -609,6 +632,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 @@ -663,8 +696,40 @@ def _passivity_validation(cls, val, values): 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 @@ -676,7 +741,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: @@ -787,8 +856,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 @@ -800,7 +869,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): @@ -845,6 +918,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", @@ -861,14 +942,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.""" @@ -966,6 +1039,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( @@ -976,17 +1050,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 @@ -997,6 +1094,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) @@ -1015,13 +1121,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 @@ -1362,6 +1491,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.""" @@ -1380,7 +1552,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: @@ -1408,7 +1584,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): @@ -1503,6 +1683,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.""" @@ -1907,12 +2090,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.""" @@ -2033,8 +2232,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 @@ -2160,12 +2359,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.""" @@ -2313,8 +2515,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 @@ -2369,6 +2571,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.""" @@ -2533,12 +2738,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.""" @@ -2642,8 +2850,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 @@ -2721,6 +2929,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.""" @@ -2735,6 +2954,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 @@ -2845,6 +3069,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 @@ -2887,8 +3121,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 @@ -3341,7 +3575,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]): @@ -3548,6 +3786,16 @@ 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] @@ -3790,7 +4038,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." ) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index c7967180c..e734fffe4 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -1887,6 +1887,11 @@ def _get_structure_plot_params(self, mat_index: int, medium: Medium) -> PlotPara 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}) @@ -3315,7 +3320,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.mediums + if isinstance(mat, AbstractCustomMedium) or mat.time_modulated + ] datasets_geometry = [] for struct in self.structures: diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index e94eed959..d6d28adc5 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -9,8 +9,8 @@ import pydantic.v1 as pydantic import numpy as np -from .base import Tidy3dBaseModel, cached_property - +from .base import cached_property +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 @@ -26,8 +26,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,133 +35,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) - - # if all time amplitudes are zero, just return (complex-valued) zeros for spectrum - if np.allclose(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] - 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, @@ -194,32 +68,11 @@ def plot_spectrum( 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 + self.plot_spectrum_in_frequency_range( + times, fmin, fmax, num_freqs=num_freqs, val=val, ax=ax, complex_fields=complex_fields + ) @abstractmethod def frequency_range(self, num_fwidth: float = 4.0) -> FreqBound: diff --git a/tidy3d/components/time.py b/tidy3d/components/time.py new file mode 100644 index 000000000..d7588bd92 --- /dev/null +++ b/tidy3d/components/time.py @@ -0,0 +1,206 @@ +""" 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_fields: bool = False, + ) -> complex: + """Complex-valued 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) + + # if all time amplitudes are zero, just return (complex-valued) zeros for spectrum + if np.allclose(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] + 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_spectrum_in_frequency_range( + self, + times: ArrayFloat1D, + fmin: float, + fmax: float, + num_freqs: int = 101, + val: PlotVal = "real", + ax: Ax = None, + complex_fields: bool = False, + ) -> Ax: + """Plot the complex-valued amplitude of the time-dependence. + + 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. + 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) + 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 + + @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 From d1b672df5966b4b1599a357d64ee9a975ca103a7 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Tue, 24 Oct 2023 15:58:40 -0500 Subject: [PATCH 19/83] Bugfix for polarization sorting with non-existent modes --- tests/test_plugins/test_mode_solver.py | 38 ++++++++++++++++++++++++++ tidy3d/plugins/mode/mode_solver.py | 12 ++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index c3b04091a..76fc74fd7 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -615,3 +615,41 @@ 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] diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index fcda3c079..b2425955c 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -305,11 +305,19 @@ def data_raw(self) -> ModeSolverData: 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(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]) + ( + 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, From e8ae0eb5db12613ce3ea9fc17d2b4896d46d54d6 Mon Sep 17 00:00:00 2001 From: Lucas <119979961+lucas-flexcompute@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:47:03 -0300 Subject: [PATCH 20/83] Add support for geometry transformations (#1203) Support is added through a new `Geometry` subclass: `Transformed` and a few convenience functions to instance it from other geometries. Related issues: - https://github.com/flexcompute/tidy3d-core/issues/25 - https://github.com/flexcompute/tidy3d-core/issues/339 Signed-off-by: Lucas Heitzmann Gabrielli --- CHANGELOG.md | 3 + tests/sims/simulation_2_5_0rc1.json | 100 + tests/sims/simulation_2_5_0rc2.h5 | Bin 0 -> 374808 bytes tests/sims/simulation_2_5_0rc2.json | 2099 +++++++++++++++++++ tests/test_components/test_geometry.py | 57 + tests/test_components/test_structure.py | 42 + tests/utils.py | 17 + tidy3d/__init__.py | 5 +- tidy3d/components/geometry/base.py | 553 ++++- tidy3d/components/geometry/mesh.py | 79 +- tidy3d/components/geometry/polyslab.py | 91 +- tidy3d/components/geometry/primitives.py | 91 +- tidy3d/components/geometry/triangulation.py | 146 ++ tidy3d/components/geometry/utils.py | 28 +- tidy3d/components/structure.py | 8 +- tidy3d/components/transformation.py | 30 +- tidy3d/components/types.py | 10 +- 17 files changed, 3250 insertions(+), 109 deletions(-) create mode 100644 tests/sims/simulation_2_5_0rc2.h5 create mode 100644 tests/sims/simulation_2_5_0rc2.json create mode 100644 tidy3d/components/geometry/triangulation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb5fa0bb..cc6eb02fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - GDSII export functions 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. - Added 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 intesections with any plane, not only axis-aligned ones. +- `Transformed` class to support for geometry transformations. +- Methods `Geometry.translated`, `Geometry.scaled`, and `Geometry.rotated` can be used to create transformed copies of any geometry. ### Changed - Update versions of `boto3`, `requests`, and `click`. diff --git a/tests/sims/simulation_2_5_0rc1.json b/tests/sims/simulation_2_5_0rc1.json index c5875ea93..97404d9ad 100644 --- a/tests/sims/simulation_2_5_0rc1.json +++ b/tests/sims/simulation_2_5_0rc1.json @@ -771,6 +771,106 @@ "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": [ diff --git a/tests/sims/simulation_2_5_0rc2.h5 b/tests/sims/simulation_2_5_0rc2.h5 new file mode 100644 index 0000000000000000000000000000000000000000..57ceb1fc6ab7001d5ac9895385eb30f98e1f4886 GIT binary patch literal 374808 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 n^(`wr+)8|>ru^AYtnhIw?O9RkUsz# None: log.info(f"Using client version: {__version__}") +Transformed.update_forward_refs() ClipOperation.update_forward_refs() GeometryGroup.update_forward_refs() @@ -156,6 +157,8 @@ def set_logging_level(level: str) -> None: "Cylinder", "PolySlab", "GeometryGroup", + "ClipOperation", + "Transformed", "TriangleMesh", "Medium", "PoleResidue", diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index ad611aac3..4aa9fac9f 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,18 +15,14 @@ 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, - Tidy3dImportError, -) +from ...exceptions import SetupError, ValidationError +from ...exceptions import Tidy3dKeyError, Tidy3dError, Tidy3dImportError from ...constants import MICROMETER, LARGE_NUMBER, RADIAN, inf, fp_eps try: @@ -45,6 +41,37 @@ 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.""" @@ -158,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 ---------- @@ -179,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 @@ -691,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 @@ -1699,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. @@ -1768,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 @@ -2020,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), + 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.""" @@ -2065,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 ---------- @@ -2092,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: @@ -2158,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] @@ -2195,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.""" @@ -2254,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 ---------- @@ -2275,7 +2719,6 @@ def intersections_plane( For more details refer to `Shapely's Documentaton `_. """ - if not self.intersects_plane(x, y, z): return [] return [ 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 9a1c8d005..763ff7ec9 100644 --- a/tidy3d/components/geometry/utils.py +++ b/tidy3d/components/geometry/utils.py @@ -2,7 +2,9 @@ from __future__ import annotations from typing import Union, Tuple -from ..types import Axis, PlanePosition, Shapely, ArrayFloat2D +import numpy as np + +from ..types import Axis, PlanePosition, Shapely, ArrayFloat2D, MatrixReal4x4 from ...exceptions import Tidy3dError from . import base @@ -12,6 +14,7 @@ GeometryType = Union[ base.Box, + base.Transformed, base.ClipOperation, base.GeometryGroup, primitives.Sphere, @@ -181,3 +184,26 @@ def vertices_from_shapely(shape: Shapely) -> ArrayFloat2D: 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 ( + np.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/structure.py b/tidy3d/components/structure.py index f1a676c55..17ba36e49 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -5,7 +5,7 @@ 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 @@ -42,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( 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 cf24e6cfa..b7a7f7422 100644 --- a/tidy3d/components/types.py +++ b/tidy3d/components/types.py @@ -1,6 +1,6 @@ """ Defines 'types' that various fields can be """ -from typing import Tuple, Union +from typing import Tuple, Union, Any # Literal only available in python 3.8 + so try import otherwise use extensions try: @@ -15,6 +15,12 @@ from shapely.geometry.base import BaseGeometry from ..exceptions import ValidationError +try: + import trimesh +except ImportError: + trimesh = None + + # type tag default name TYPE_TAG_STR = "type" @@ -134,6 +140,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 """ @@ -194,6 +201,7 @@ def __modify_schema__(cls, field_schema): 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 """ From c90e319b9ce598a75f4fe5d48f22640f61d10cc5 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Thu, 26 Oct 2023 13:13:18 -0400 Subject: [PATCH 21/83] handle case with no adjoint sources --- CHANGELOG.md | 1 + tests/test_plugins/test_adjoint.py | 47 +++++++++++++++++++ .../adjoint/components/data/sim_data.py | 29 ++++++++++++ 3 files changed, 77 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc6eb02fc..ee9281bea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove warning that monitors now have `colocate=True` by default. ### Fixed +- If no adjoint sources for one simulation in an objective function, make a mock source with zero amplitude and warn user. ## [2.5.0rc1] - 2023-10-10 diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index 9fd7d0137..8d7f2c05b 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -1568,3 +1568,50 @@ def test_adjoint_run_time(use_emulated_run, tmp_path, fwidth, run_time, run_time 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) diff --git a/tidy3d/plugins/adjoint/components/data/sim_data.py b/tidy3d/plugins/adjoint/components/data/sim_data.py index a4b28304a..8b384ae9f 100644 --- a/tidy3d/plugins/adjoint/components/data/sim_data.py +++ b/tidy3d/plugins/adjoint/components/data/sim_data.py @@ -11,6 +11,8 @@ 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 @@ -173,6 +175,33 @@ def make_adjoint_simulation(self, fwidth: float, run_time: float) -> JaxSimulati for adj_source in mnt_data_vjp.to_adjoint_sources(fwidth=fwidth): adj_srcs.append(adj_source) + # 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, From 0aae0e0ff3fd8ad702af4609061720b8ef81e7b6 Mon Sep 17 00:00:00 2001 From: momchil Date: Fri, 20 Oct 2023 16:41:53 -0700 Subject: [PATCH 22/83] Setting protocol_version to None if solver_version provided --- tidy3d/web/core/task_core.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tidy3d/web/core/task_core.py b/tidy3d/web/core/task_core.py index 1c59f0728..a37533987 100644 --- a/tidy3d/web/core/task_core.py +++ b/tidy3d/web/core/task_core.py @@ -413,12 +413,18 @@ def submit( worker_group: str = None worker group """ + + if solver_version: + protocol_version = None + else: + protocol_version = http_util.get_version() + http.post( f"tidy3d/tasks/{self.task_id}/submit", { "solverVersion": solver_version, "workerGroup": worker_group, - "protocolVersion": http_util.get_version(), + "protocolVersion": protocol_version, }, ) From 76a4ac11c6e2e43c9ef9604acd9ac4974b4cb54d Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Wed, 18 Oct 2023 12:47:15 -0500 Subject: [PATCH 23/83] solver classes unification --- tests/test_components/test_heat.py | 3 +- tests/test_components/test_scene.py | 61 +- tests/test_components/test_simulation.py | 29 +- tidy3d/components/base_sim/data/sim_data.py | 2 - tidy3d/components/base_sim/monitor.py | 143 +-- tidy3d/components/base_sim/simulation.py | 44 +- tidy3d/components/base_sim/source.py | 21 + tidy3d/components/data/monitor_data.py | 9 +- tidy3d/components/data/sim_data.py | 80 +- tidy3d/components/heat/source.py | 5 +- tidy3d/components/monitor.py | 87 +- tidy3d/components/scene.py | 210 +++-- tidy3d/components/simulation.py | 909 ++------------------ tidy3d/components/source.py | 9 +- 14 files changed, 300 insertions(+), 1312 deletions(-) create mode 100644 tidy3d/components/base_sim/source.py diff --git a/tests/test_components/test_heat.py b/tests/test_components/test_heat.py index d1792e233..988052494 100644 --- a/tests/test_components/test_heat.py +++ b/tests/test_components/test_heat.py @@ -26,7 +26,6 @@ from tidy3d import TemperatureMonitor from tidy3d import TemperatureData -from tidy3d.exceptions import DataError from ..utils import STL_GEO, assert_log_level, log_capture @@ -349,7 +348,7 @@ def test_sim_data(): _ = heat_sim_data.plot_field("test", z=0) plt.close() - with pytest.raises(DataError): + with pytest.raises(KeyError): _ = heat_sim_data.plot_field("test3", x=0) with pytest.raises(pd.ValidationError): diff --git a/tests/test_components/test_scene.py b/tests/test_components/test_scene.py index 3c6c5939b..3310c9ff6 100644 --- a/tests/test_components/test_scene.py +++ b/tests/test_components/test_scene.py @@ -5,7 +5,7 @@ import numpy as np import tidy3d as td -from tidy3d.components.simulation import MAX_NUM_MEDIUMS +from tidy3d.components.scene import MAX_NUM_MEDIUMS, MAX_GEOMETRY_COUNT from ..utils import assert_log_level, log_capture, SIM_FULL SCENE = td.Scene() @@ -187,32 +187,6 @@ def test_names_unique(): ) -def test_allow_gain(): - """Test if simulation allows gain.""" - - medium = td.Medium(permittivity=2.0) - medium_gain = td.Medium(permittivity=2.0, allow_gain=True) - medium_ani = td.AnisotropicMedium(xx=medium, yy=medium, zz=medium) - medium_gain_ani = td.AnisotropicMedium(xx=medium, yy=medium_gain, zz=medium) - - # Test simulation medium - scene = td.Scene(medium=medium) - assert not scene.allow_gain - scene = scene.updated_copy(medium=medium_gain) - assert scene.allow_gain - - # Test structure with anisotropic gain medium - struct = td.Structure(geometry=td.Box(center=(0, 0, 0), size=(1, 1, 1)), medium=medium_ani) - struct_gain = struct.updated_copy(medium=medium_gain_ani) - scene = td.Scene( - medium=medium, - structures=[struct], - ) - assert not scene.allow_gain - scene = scene.updated_copy(structures=[struct_gain]) - assert scene.allow_gain - - def test_perturbed_mediums_copy(): # Non-dispersive @@ -272,3 +246,36 @@ def test_perturbed_mediums_copy(): 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 e5a1995d0..d0d0a13d6 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -8,7 +8,8 @@ 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 @@ -492,7 +493,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 @@ -538,7 +538,7 @@ def test_validate_mnt_size(monkeypatch, log_capture): 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( @@ -566,7 +566,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) @@ -582,7 +582,6 @@ def test_plot_structure(): 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() @@ -725,26 +724,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( diff --git a/tidy3d/components/base_sim/data/sim_data.py b/tidy3d/components/base_sim/data/sim_data.py index 3338d7378..be2172d95 100644 --- a/tidy3d/components/base_sim/data/sim_data.py +++ b/tidy3d/components/base_sim/data/sim_data.py @@ -41,8 +41,6 @@ class AbstractSimulationData(Tidy3dBaseModel, ABC): def __getitem__(self, monitor_name: str) -> AbstractMonitorData: """Get a :class:`.AbstractMonitorData` by name. Apply symmetry if applicable.""" - if monitor_name not in self.monitor_data: - raise DataError(f"'{self.type}' does not contain data for monitor '{monitor_name}'.") monitor_data = self.monitor_data[monitor_name] return monitor_data.symmetry_expanded_copy diff --git a/tidy3d/components/base_sim/monitor.py b/tidy3d/components/base_sim/monitor.py index 65cc1ddc0..ff13887fd 100644 --- a/tidy3d/components/base_sim/monitor.py +++ b/tidy3d/components/base_sim/monitor.py @@ -6,14 +6,10 @@ import numpy as np from ..types import ArrayFloat1D, Numpy -from ..types import Direction, Axis, BoxSurface +from ..types import Axis from ..geometry.base import Box -from ..validators import assert_plane from ..base import cached_property from ..viz import PlotParams, plot_params_monitor -from ...constants import SECOND -from ...exceptions import SetupError -from ...log import log class AbstractMonitor(Box, ABC): @@ -94,140 +90,3 @@ def downsampled_num_cells(self, num_cells: Tuple[int, int, int]) -> Tuple[int, i """ arrs = [np.arange(ncells) for ncells in num_cells] return tuple((self.downsample(arr, axis=dim).size for dim, arr in enumerate(arrs))) - - -class AbstractTimeMonitor(AbstractMonitor, ABC): - """Abstract base class for transient monitors.""" - - start: pd.NonNegativeFloat = pd.Field( - 0.0, - title="Start time", - description="Time at which to start monitor recording.", - units=SECOND, - ) - - stop: pd.NonNegativeFloat = pd.Field( - None, - title="Stop time", - description="Time at which to stop monitor recording. " - "If not specified, record until end of simulation.", - units=SECOND, - ) - - interval: pd.PositiveInt = pd.Field( - 1, - title="Time interval", - description="Number of time step intervals between monitor recordings.", - ) - - @pd.validator("stop", always=True, allow_reuse=True) - def stop_greater_than_start(cls, val, values): - """Ensure sure stop is greater than or equal to start.""" - start = values.get("start") - if val and val < start: - raise SetupError("Monitor start time is greater than stop time.") - return val - - def time_inds(self, tmesh: ArrayFloat1D) -> Tuple[int, int]: - """Compute the starting and stopping index of the monitor in a given discrete time mesh.""" - - tmesh = np.array(tmesh) - tind_beg, tind_end = (0, 0) - - if tmesh.size == 0: - return (tind_beg, tind_end) - - # If monitor.stop is None, record until the end - t_stop = self.stop - if t_stop is None: - tind_end = int(tmesh.size) - t_stop = tmesh[-1] - else: - tend = np.nonzero(tmesh <= t_stop)[0] - if tend.size > 0: - tind_end = int(tend[-1] + 1) - - # Step to compare to in order to handle t_start = t_stop - dt = 1e-20 if np.array(tmesh).size < 2 else tmesh[1] - tmesh[0] - # If equal start and stopping time, record one time step - if np.abs(self.start - t_stop) < dt and self.start <= tmesh[-1]: - tind_beg = max(tind_end - 1, 0) - else: - tbeg = np.nonzero(tmesh[:tind_end] >= self.start)[0] - tind_beg = tbeg[0] if tbeg.size > 0 else tind_end - return (tind_beg, tind_end) - - def num_steps(self, tmesh: ArrayFloat1D) -> int: - """Compute number of time steps for a time monitor.""" - - tind_beg, tind_end = self.time_inds(tmesh) - return int((tind_end - tind_beg) / self.interval) - - -class AbstractPlanarMonitor(AbstractMonitor, ABC): - """:class:`AbstractMonitor` that has a planar geometry.""" - - _plane_validator = assert_plane() - - @cached_property - def normal_axis(self) -> Axis: - """Axis normal to the monitor's plane.""" - return self.size.index(0.0) - - -class AbstractSurfaceIntegrationMonitor(AbstractMonitor, ABC): - """Abstract class for monitors that perform surface integrals during the solver run.""" - - normal_dir: Direction = pd.Field( - None, - 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[BoxSurface, ...] = pd.Field( - None, - title="Excluded surfaces", - description="Surfaces to exclude in the integration, if a volume monitor.", - ) - - @property - def integration_surfaces(self): - """Surfaces of the monitor where fields will be recorded for subsequent integration.""" - if self.size.count(0.0) == 0: - return self.surfaces_with_exclusion(**self.dict()) - return [self] - - @pd.root_validator(skip_on_failure=True) - def normal_dir_exists_for_surface(cls, values): - """If the monitor is a surface, set default ``normal_dir`` if not provided. - If the monitor is a box, warn that ``normal_dir`` is relevant only for surfaces.""" - normal_dir = values.get("normal_dir") - name = values.get("name") - size = values.get("size") - if size.count(0.0) != 1: - if normal_dir is not None: - log.warning( - "The ``normal_dir`` field is relevant only for surface monitors " - f"and will be ignored for monitor {name}, which is a box." - ) - else: - if normal_dir is None: - values["normal_dir"] = "+" - return values - - @pd.root_validator(skip_on_failure=True) - def check_excluded_surfaces(cls, values): - """Error if ``exclude_surfaces`` is provided for a surface monitor.""" - exclude_surfaces = values.get("exclude_surfaces") - if exclude_surfaces is None: - return values - name = values.get("name") - size = values.get("size") - if size.count(0.0) > 0: - raise SetupError( - f"Can't specify ``exclude_surfaces`` for surface monitor {name}; " - "valid for box monitors only." - ) - return values diff --git a/tidy3d/components/base_sim/simulation.py b/tidy3d/components/base_sim/simulation.py index a08b55efb..a8631cf82 100644 --- a/tidy3d/components/base_sim/simulation.py +++ b/tidy3d/components/base_sim/simulation.py @@ -7,7 +7,7 @@ import pydantic.v1 as pd -from .monitor import AbstractSurfaceIntegrationMonitor +from .monitor import AbstractMonitor from ..base import cached_property from ..validators import assert_unique_names, assert_objects_in_sim_bounds @@ -22,7 +22,7 @@ from ..viz import PlotParams, plot_params_symmetry from ...version import __version__ -from ...exceptions import Tidy3dKeyError, SetupError +from ...exceptions import Tidy3dKeyError from ...log import log @@ -91,30 +91,11 @@ class AbstractSimulation(Box, ABC): # 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("monitors", always=True) - def _integration_surfaces_in_bounds(cls, val, values): - """Error if any of the integration surfaces are outside of the simulation domain.""" - - if val is None: - return val - - sim_center = values.get("center") - sim_size = values.get("size") - sim_box = Box(size=sim_size, center=sim_center) - - for mnt in (mnt for mnt in val if isinstance(mnt, AbstractSurfaceIntegrationMonitor)): - if not any(sim_box.intersects(surf) for surf in mnt.integration_surfaces): - raise SetupError( - f"All integration surfaces of monitor '{mnt.name}' are outside of the " - "simulation bounds." - ) - - return val - @pd.validator("structures", always=True) def _structures_not_at_edges(cls, val, values): """Warn if any structures lie at the simulation boundaries.""" @@ -135,14 +116,21 @@ def _structures_not_at_edges(cls, val, values): 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. " + 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." + "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 @@ -151,7 +139,7 @@ def scene(self) -> Scene: return Scene(medium=self.medium, structures=self.structures) - def get_monitor_by_name(self, name: str): # -> Monitor: + def get_monitor_by_name(self, name: str) -> AbstractMonitor: """Return monitor named 'name'.""" for monitor in self.monitors: if monitor.name == name: @@ -271,7 +259,7 @@ def plot_sources( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - bounds = self.simulation_bounds + 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( @@ -315,7 +303,7 @@ def plot_monitors( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ - bounds = self.simulation_bounds + 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( 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/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index eb0b0a95a..71e966273 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() diff --git a/tidy3d/components/data/sim_data.py b/tidy3d/components/data/sim_data.py index 74f67eb73..f52fe2d99 100644 --- a/tidy3d/components/data/sim_data.py +++ b/tidy3d/components/data/sim_data.py @@ -1,26 +1,27 @@ """ Simulation Level Data """ from __future__ import annotations -from typing import Dict, Callable, Tuple +from typing import Callable, Tuple import xarray as xr import pydantic.v1 as pd import numpy as np 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_sim.data.sim_data import AbstractSimulationData + DATA_TYPE_MAP = {data.__fields__["monitor"].type_: data for data in MonitorDataTypes} -class SimulationData(Tidy3dBaseModel): +class SimulationData(AbstractSimulationData): """Stores data from a collection of :class:`.Monitor` objects in a :class:`.Simulation`. Example @@ -75,45 +76,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.""" @@ -315,44 +283,6 @@ 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): """return ``xarray.DataArray`` of the scalar field of a given monitor at Yee cell centers. diff --git a/tidy3d/components/heat/source.py b/tidy3d/components/heat/source.py index b72f6993a..c47735303 100644 --- a/tidy3d/components/heat/source.py +++ b/tidy3d/components/heat/source.py @@ -8,14 +8,15 @@ from .viz import plot_params_heat_source -from ..base import Tidy3dBaseModel, cached_property +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(ABC, Tidy3dBaseModel): +class HeatSource(AbstractSource, ABC): """Abstract heat source.""" structures: Tuple[str, ...] = pd.Field( diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index bd2a5ced2..59b0d72bf 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -1,22 +1,23 @@ """Objects that define how data is recorded from simulation.""" -from abc import ABC, abstractmethod +from abc import ABC from typing import Union, Tuple import pydantic.v1 as pydantic import numpy as np -from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound, Numpy +from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound from .types import Literal, Direction, Coordinate, Axis, ObsGridArray, BoxSurface -from .geometry.base import Box from .validators import assert_plane 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 .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 @@ -24,16 +25,9 @@ WARN_NUM_MODES = 100 -class Monitor(Box, ABC): +class Monitor(AbstractMonitor): """Abstract base class for monitors.""" - name: str = pydantic.Field( - ..., - title="Name", - description="Unique name for monitor.", - min_length=1, - ) - interval_space: Tuple[Literal[1], Literal[1], Literal[1]] = pydantic.Field( (1, 1, 1), title="Spatial interval", @@ -51,75 +45,6 @@ class Monitor(Box, ABC): "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 = 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))) - class FreqMonitor(Monitor, ABC): """:class:`Monitor` that records data in the frequency-domain.""" diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py index ee167233f..2bb5cc503 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -11,14 +11,14 @@ from .base import cached_property, Tidy3dBaseModel from .validators import assert_unique_names -from .geometry.base import Box -from .geometry.mesh import TriangleMesh +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, PECMedium from .medium import AbstractCustomMedium, Medium2D, MediumType3D -from .medium import AnisotropicMedium, AbstractPerturbationMedium +from .medium import AbstractPerturbationMedium +from .grid.grid import Grid from .structure import Structure -from .data.dataset import Dataset from .data.data_array import SpatialDataArray from .viz import add_ax_if_none, equal_aspect from .grid.grid import Coords @@ -34,6 +34,9 @@ # 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 @@ -88,6 +91,29 @@ def _validate_num_mediums(cls, val): 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 @@ -357,13 +383,13 @@ def plot_structures( The supplied or created matplotlib axes. """ - medium_shapes = self._get_structures_plane(structures=self.structures, x=x, y=y, z=z) + 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 @@ -392,6 +418,11 @@ def _get_structure_plot_params(self, mat_index: int, medium: Medium) -> PlotPara 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}) @@ -448,11 +479,16 @@ def _set_plot_bounds( ax.set_ylim(vlim) return ax - @staticmethod - def _get_structures_plane( - structures: List[Structure], x: float = None, y: float = None, z: float = None + 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 plane specified by {x,y,z}. + """Compute list of shapes to plot on 2d box specified by (x_min, x_max), (y_min, y_max). Parameters ---------- @@ -464,17 +500,42 @@ def _get_structures_plane( 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 = structure.geometry.intersections_plane(x=x, y=y, z=z) - if len(intersections) > 0: - for shape in intersections: + 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 @@ -649,6 +710,7 @@ def plot_structures_eps( 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. @@ -688,7 +750,6 @@ def plot_structures_eps( """ structures = self.structures - structures = [self.background_structure] + list(structures) # alpha is None just means plot without any transparency if alpha is None: @@ -704,7 +765,10 @@ def plot_structures_eps( plane = Box(center=center, size=size) medium_shapes = self._filter_structures_plane_medium(structures=structures, plane=plane) else: - medium_shapes = self._get_structures_plane(structures=structures, x=x, y=y, z=z) + 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 @@ -737,7 +801,7 @@ def plot_structures_eps( 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 + x, y, z, freq, alpha, medium, eps_min, eps_max, reverse, shape, ax, grid ) if cbar: @@ -779,11 +843,10 @@ def eps_bounds(self, freq: float = None) -> Tuple[float, float]: eps_list = [ medium.eps_model(freq).real for medium in medium_list - if not isinstance(medium, AbstractCustomMedium) + if not isinstance(medium, AbstractCustomMedium) and not isinstance(medium, Medium2D) ] - eps_list.append(1) - eps_min = min(eps_list) - eps_max = max(eps_list) + 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)]: @@ -811,6 +874,7 @@ def _pcolormesh_shape_custom_medium_structure_eps( reverse: bool, shape: Shapely, ax: Ax, + grid: Grid, ): """ Plot shape made of custom medium with ``pcolormesh``. @@ -818,8 +882,6 @@ def _pcolormesh_shape_custom_medium_structure_eps( 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) - plane_axes_inds = [0, 1, 2] - plane_axes_inds.pop(normal_axis_ind) # make grid for eps interpolation # we will do this by combining shape bounds and points where custom eps is provided @@ -828,31 +890,54 @@ def _pcolormesh_shape_custom_medium_structure_eps( rmin.insert(normal_axis_ind, normal_position) rmax.insert(normal_axis_ind, normal_position) - # 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] + 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: - 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) + 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 = { @@ -1035,7 +1120,6 @@ def plot_structures_heat_conductivity( """ structures = self.structures - structures = [self.background_structure] + list(structures) # alpha is None just means plot without any transparency if alpha is None: @@ -1051,7 +1135,10 @@ def plot_structures_heat_conductivity( plane = Box(center=center, size=size) medium_shapes = self._filter_structures_plane_medium(structures=structures, plane=plane) else: - medium_shapes = self._get_structures_plane(structures=structures, x=x, y=y, z=z) + 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: @@ -1154,31 +1241,6 @@ def _plot_shape_structure_heat_cond( """ Misc """ - @property - def custom_datasets(self) -> List[Dataset]: - """List of custom datasets for verification purposes. If the list is not empty, then - the scene needs to be exported to hdf5 to store the data. - """ - datasets_medium = [mat for mat in self.mediums if isinstance(mat, AbstractCustomMedium)] - datasets_geometry = [ - struct.geometry.mesh_dataset - for struct in self.structures - if isinstance(struct.geometry, TriangleMesh) - ] - return datasets_medium + datasets_geometry - - @cached_property - def allow_gain(self) -> bool: - """``True`` if any of the mediums in the scene allows gain.""" - - for medium in self.mediums: - if isinstance(medium, AnisotropicMedium): - if np.any([med.allow_gain for med in [medium.xx, medium.yy, medium.zz]]): - return True - elif medium.allow_gain: - return True - return False - def perturbed_mediums_copy( self, temperature: SpatialDataArray = None, diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index e734fffe4..90a70cd8f 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -2,24 +2,21 @@ 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, InterpMethod +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 @@ -39,16 +36,17 @@ 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, Tidy3dImportError +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 @@ -65,15 +63,9 @@ # 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 @@ -95,7 +87,7 @@ PML_HEIGHT_FOR_0_DIMS = 0.02 -class Simulation(Box): +class Simulation(AbstractSimulation): """Contains all information about Tidy3d simulation. Example @@ -154,33 +146,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", @@ -242,12 +207,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) @@ -269,17 +228,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() @@ -445,22 +397,6 @@ def boundaries_for_zero_dims(cls, val, values): ) 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.""" @@ -477,58 +413,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.""" @@ -944,6 +828,7 @@ def _check_normalize_index(cls, val, values): 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() @@ -1245,9 +1130,7 @@ 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()) + return self.scene.mediums @cached_property def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: @@ -1261,20 +1144,12 @@ 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}'") + return self.scene.medium_map @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) + return self.scene.background_structure @staticmethod def intersecting_media( @@ -1296,19 +1171,7 @@ 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 + return Scene.intersecting_media(test_object=test_object, structures=structures) @staticmethod def intersecting_structures( @@ -1330,26 +1193,7 @@ 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 + 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. @@ -1718,28 +1562,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 @@ -1791,30 +1625,29 @@ 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 = 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 @@ -1852,64 +1685,11 @@ def plot_structures( 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 + hlim_new, vlim_new = Scene._get_plot_lims( + bounds=self.simulation_bounds, 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) - 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 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(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$") + 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 @@ -1961,292 +1741,27 @@ def plot_structures_eps( 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(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, + hlim, vlim = Scene._get_plot_lims( + bounds=self.simulation_bounds, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) - 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 + 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, ) - 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 + def eps_bounds(self, freq: float = None) -> Tuple[float, float]: + """Compute range of (real) permittivity present in the simulation at frequency "freq".""" + return self.scene.eps_bounds(freq=freq) @cached_property def num_pml_layers(self) -> List[Tuple[float, float]]: @@ -2294,12 +1809,9 @@ 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) + def simulation_bounds(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: + """Simulation bounds including the PML regions.""" + return self.bounds_pml @equal_aspect @add_ax_if_none @@ -2339,7 +1851,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]: @@ -2374,77 +1888,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, @@ -2516,7 +1959,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 @@ -2637,217 +2082,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. @@ -3255,7 +2489,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) @@ -3567,15 +2801,6 @@ def perturbed_mediums_copy( return Simulation.parse_obj(sim_dict) - @cached_property - def scene(self) -> Scene: - """Return a :class:.`Scene` instance based on the current simulation.""" - - return Scene( - structures=self.structures, - medium=self.medium, - ) - @classmethod def from_scene(cls, scene: Scene, **kwargs) -> Simulation: """Create a simulation from a :class:.`Scene` instance. Must provide additional parameters diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index d6d28adc5..8d669d8d5 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -10,10 +10,11 @@ import numpy as np 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 assert_plane, assert_volumetric, get_value from .validators import warn_if_dataset_none, assert_single_freq_in_range from .data.dataset import FieldDataset, TimeDataset from .data.data_array import TimeDataArray @@ -323,7 +324,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( @@ -333,15 +334,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.""" From 11b59e7ec3180972c8d85810acba1ad595e2e737 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Thu, 26 Oct 2023 20:20:19 -0500 Subject: [PATCH 24/83] PR comments --- tests/test_components/test_scene.py | 8 +- tests/test_components/test_simulation.py | 14 +- tidy3d/components/base_sim/simulation.py | 174 ++++++++++++++++++++++- tidy3d/components/simulation.py | 121 ++++++++-------- 4 files changed, 250 insertions(+), 67 deletions(-) diff --git a/tests/test_components/test_scene.py b/tests/test_components/test_scene.py index 3310c9ff6..d54ba52fb 100644 --- a/tests/test_components/test_scene.py +++ b/tests/test_components/test_scene.py @@ -16,7 +16,7 @@ def test_scene_init(): """make sure a scene can be initialized""" - sim = td.Scene( + scene = td.Scene( structures=[ td.Structure( geometry=td.Box(size=(1, 1, 1), center=(-1, 0, 0)), @@ -37,9 +37,9 @@ def test_scene_init(): medium=td.Medium(permittivity=3.0), ) - _ = sim.mediums - _ = sim.medium_map - _ = sim.background_structure + _ = scene.mediums + _ = scene.medium_map + _ = scene.background_structure def test_validate_components_none(): diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index d0d0a13d6..426fd7b62 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -79,10 +79,15 @@ 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) @@ -915,11 +920,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), diff --git a/tidy3d/components/base_sim/simulation.py b/tidy3d/components/base_sim/simulation.py index a8631cf82..7988f14b3 100644 --- a/tidy3d/components/base_sim/simulation.py +++ b/tidy3d/components/base_sim/simulation.py @@ -165,7 +165,7 @@ def simulation_geometry(self) -> Box: @cached_property def simulation_structure(self) -> Structure: """Returns structure representing the domain of the simulation. This differs from - ``Simulation.background_structure`` in that it has finite extent.""" + ``Simulation.scene.background_structure`` in that it has finite extent.""" return Structure(geometry=self.simulation_geometry, medium=self.medium) @equal_aspect @@ -416,6 +416,178 @@ def plot_boundaries( 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 diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 90a70cd8f..fdf3290e9 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -265,7 +265,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: @@ -322,7 +322,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 @@ -628,7 +628,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( @@ -698,7 +698,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): @@ -783,7 +783,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( @@ -1063,7 +1063,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 ) @@ -1121,6 +1121,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. @@ -1130,8 +1131,13 @@ def mediums(self) -> Set[MediumType]: List[:class:`.AbstractMedium`] Set of distinct mediums in the simulation. """ + 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. @@ -1144,13 +1150,24 @@ def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: Mapping between distinct mediums to index in simulation. """ + 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`.""" + + 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, ...] @@ -1171,8 +1188,14 @@ def intersecting_media( List[:class:`.AbstractMedium`] Set of distinct mediums that intersect with the given planar object. """ + + 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, ...] @@ -1193,6 +1216,11 @@ def intersecting_structures( Set of distinct structures that intersect with the given surface, or with the surfaces of the given volume. """ + + 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): @@ -1208,7 +1236,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 @@ -1651,46 +1679,6 @@ def plot_eps( 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. - """ - - 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( @@ -1759,8 +1747,14 @@ def plot_structures_eps( reverse=reverse, ) + # 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".""" + + 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 @@ -1798,8 +1792,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 @@ -1808,11 +1812,6 @@ def bounds_pml(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, fl return (bounds_min, bounds_max) - @cached_property - def simulation_bounds(self) -> Tuple[Tuple[float, float, float], Tuple[float, float, float]]: - """Simulation bounds including the PML regions.""" - return self.bounds_pml - @equal_aspect @add_ax_if_none def plot_pml( @@ -1872,7 +1871,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: @@ -1932,8 +1931,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] @@ -2124,7 +2123,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 @@ -2556,7 +2555,7 @@ def custom_datasets(self) -> List[Dataset]: ] datasets_medium = [ mat - for mat in self.mediums + for mat in self.scene.mediums if isinstance(mat, AbstractCustomMedium) or mat.time_modulated ] datasets_geometry = [] @@ -2578,7 +2577,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]: @@ -2643,7 +2642,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. " @@ -2693,7 +2692,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 @@ -2734,7 +2733,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, From 1e5d85e276f5cd1ef6a13b6bb1e436a021e2ade4 Mon Sep 17 00:00:00 2001 From: Shashwat Sharma Date: Mon, 30 Oct 2023 12:12:33 -0400 Subject: [PATCH 25/83] for PML or absorbers along a zero dim, raise error instead of warning - update changelog --- CHANGELOG.md | 1 + tests/test_components/test_simulation.py | 29 ++++++++++++------------ tests/test_plugins/test_adjoint.py | 5 ++++ tidy3d/components/simulation.py | 11 ++++----- 4 files changed, 26 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee9281bea..35d341597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update versions of `boto3`, `requests`, and `click`. - python 3.7 no longer tested nor supported. - Remove 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. ### Fixed - If no adjoint sources for one simulation in an objective function, make a mock source with zero amplitude and warn user. diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 426fd7b62..d0a99b6fc 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -461,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), @@ -470,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, @@ -490,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(), ), ) @@ -611,6 +611,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() diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index 8d7f2c05b..a03cfbfa9 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -849,6 +849,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(), + ), ) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index fdf3290e9..8b3fbae01 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -383,17 +383,16 @@ 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 From 1c9dd24c4af87a1903d7985d645b08905c88a5e4 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 30 Oct 2023 16:16:11 -0400 Subject: [PATCH 26/83] prep for 2.5.0rc2 --- CHANGELOG.md | 21 +- tests/sims/simulation_2_5_0rc2.h5 | Bin 374808 -> 374808 bytes tests/sims/simulation_2_5_0rc2.json | 36 +- tidy3d/components/geometry/base.py | 2 +- tidy3d/schema.json | 725 +++++++++++++++++++++++----- 5 files changed, 625 insertions(+), 159 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35d341597..05d58a11e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,25 +3,25 @@ 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.0rc2] - 2023-10-30 ### Added - Support for multiple frequencies in `output_monitors` in `adjoint` plugin. -- GDSII export functions 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. -- Added 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 intesections with any plane, not only axis-aligned ones. -- `Transformed` class to support for geometry transformations. +- 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. ### Changed -- Update versions of `boto3`, `requests`, and `click`. +- Updated versions of `boto3`, `requests`, and `click`. - python 3.7 no longer tested nor supported. -- Remove warning that monitors now have `colocate=True` by default. +- 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. ### Fixed -- If no adjoint sources for one simulation in an objective function, make a mock source with zero amplitude and warn user. +- 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.5.0rc1] - 2023-10-10 @@ -1003,7 +1003,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.5.0rc1...pre/2.5 +[Unreleased]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc2...pre/2.5 +[2.5.0rc2]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc1...v2.5.0rc2 [2.5.0rc1]: https://github.com/flexcompute/tidy3d/compare/v2.4.2...v2.5.0rc1 [Unreleased]: https://github.com/flexcompute/tidy3d/compare/v2.4.3...develop [2.4.3]: https://github.com/flexcompute/tidy3d/compare/v2.4.2...v2.4.3 diff --git a/tests/sims/simulation_2_5_0rc2.h5 b/tests/sims/simulation_2_5_0rc2.h5 index 57ceb1fc6ab7001d5ac9895385eb30f98e1f4886..6897bbaa28dd5de7b133f07842bd378d5fa94367 100644 GIT binary patch delta 354 zcmbR7L2Skcu?;6VCYQMgPkt#aHkp(2(`FN{=_!+cb6QS5ou@Q;orC1$0te2?elOWK zFX-`OoE+DFezIN=+hpGk>B%95W-tYtCl!h^AxQ{K{#2|6msBW`W}0l)sIj@WERG2- z`nW=!X>vgs@8r0~=*d0RaOFTdI5xA_1I;sQTs3)iT@YNy*E)5k&1Q{hj4+YO1-@*X zS2am9ZNA^4j4CS7CJT1ZX5S76xXk9Moj_wDb}CN()IA?=5X6HW$bOLB+|YkDcJjuz zBH}t+3P7M#RGJrGl9`*TWTjx3s%vO8+3#%(6Ibo@`!Y8=Hk6G2 delta 326 zcmbR7L2Skcu?;6Vgo;Y@;!84fQK^arvi=jXlMCL4%Ia__0D)3jYEf}!ejYF=l#KLD^$d!Vjg+`*r{9-hl5BqPw*A3d RMj&PaV&?4+-m+Mn0sydBfaU-I diff --git a/tests/sims/simulation_2_5_0rc2.json b/tests/sims/simulation_2_5_0rc2.json index f11466f44..70702aab5 100644 --- a/tests/sims/simulation_2_5_0rc2.json +++ b/tests/sims/simulation_2_5_0rc2.json @@ -10,7 +10,6 @@ 8.0, 8.0 ], - "run_time": 1e-12, "medium": { "name": null, "frequency_range": null, @@ -22,11 +21,6 @@ "permittivity": 1.0, "conductivity": 0.0 }, - "symmetry": [ - 0, - 0, - 0 - ], "structures": [ { "geometry": { @@ -902,8 +896,14 @@ } } ], + "symmetry": [ + 0, + 0, + 0 + ], "sources": [ { + "name": null, "type": "UniformCurrentSource", "center": [ 0.0, @@ -924,11 +924,11 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "interpolate": true, "polarization": "Hx" }, { + "name": null, "type": "PointDipole", "center": [ 0.0, @@ -949,11 +949,11 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "interpolate": true, "polarization": "Ex" }, { + "name": null, "type": "ModeSource", "center": [ 0.0, @@ -974,7 +974,6 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "num_freqs": 1, "direction": "-", "mode_spec": { @@ -997,6 +996,7 @@ "mode_index": 0 }, { + "name": null, "type": "PlaneWave", "center": [ 0.0, @@ -1017,13 +1017,13 @@ "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, @@ -1044,7 +1044,6 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, @@ -1054,6 +1053,7 @@ "waist_distance": 0.0 }, { + "name": null, "type": "AstigmaticGaussianBeam", "center": [ 0.0, @@ -1074,7 +1074,6 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "num_freqs": 1, "direction": "+", "angle_theta": 0.0, @@ -1090,6 +1089,7 @@ ] }, { + "name": null, "type": "CustomFieldSource", "center": [ 0.0, @@ -1110,7 +1110,6 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "field_dataset": { "type": "FieldDataset", "Ex": "ScalarFieldDataArray", @@ -1122,6 +1121,7 @@ } }, { + "name": null, "type": "CustomCurrentSource", "center": [ 0.0, @@ -1142,7 +1142,6 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "interpolate": true, "current_dataset": { "type": "FieldDataset", @@ -1155,6 +1154,7 @@ } }, { + "name": null, "type": "TFSF", "center": [ 1.0, @@ -1175,7 +1175,6 @@ "offset": 5.0, "remove_dc_component": true }, - "name": null, "direction": "+", "angle_theta": 0.5235987755982988, "angle_phi": 0.6283185307179586, @@ -1183,6 +1182,7 @@ "injection_axis": 2 }, { + "name": null, "type": "UniformCurrentSource", "center": [ 0.0, @@ -1206,7 +1206,6 @@ "values": "TimeDataArray" } }, - "name": null, "interpolate": true, "polarization": "Hx" } @@ -2091,9 +2090,10 @@ ], "type": "GridSpec" }, + "version": "2.5.0rc2", + "run_time": 1e-12, "shutoff": 0.0001, "subpixel": false, "normalize_index": 0, - "courant": 0.8, - "version": "2.5.0rc2" + "courant": 0.8 } \ No newline at end of file diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index 4aa9fac9f..5592402c5 100644 --- a/tidy3d/components/geometry/base.py +++ b/tidy3d/components/geometry/base.py @@ -2189,7 +2189,7 @@ class Transformed(Geometry): ) transform: MatrixReal4x4 = pydantic.Field( - np.eye(4), + np.eye(4).tolist(), title="Transform", description="Transform matrix applied to the base geometry.", ) diff --git a/tidy3d/schema.json b/tidy3d/schema.json index 82b51abfa..fdbe241ed 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, heat_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.5.0rc1\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.0rc2\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,7 @@ "frequency_range": null, "allow_gain": false, "nonlinear_spec": null, + "modulation_spec": null, "heat_spec": null, "type": "Medium", "permittivity": 1.0, @@ -157,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.", @@ -195,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.", @@ -478,6 +472,19 @@ } ] }, + "version": { + "title": "Version", + "description": "String specifying the front end version number.", + "default": "2.5.0rc2", + "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.", @@ -505,12 +512,6 @@ "exclusiveMinimum": 0.0, "maximum": 1.0, "type": "number" - }, - "version": { - "title": "Version", - "description": "String specifying the front end version number.", - "default": "2.5.0rc1", - "type": "string" } }, "required": [ @@ -551,6 +552,196 @@ ], "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()", @@ -603,7 +794,7 @@ }, "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.\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)", + "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 : Optional[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": { @@ -632,7 +823,7 @@ }, "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" }, @@ -645,6 +836,15 @@ } ] }, + "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``.", @@ -711,7 +911,7 @@ }, "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.\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)", + "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 : Optional[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": { @@ -740,7 +940,7 @@ }, "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" }, @@ -753,6 +953,15 @@ } ] }, + "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``.", @@ -862,7 +1071,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.\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)", + "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 : Optional[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": { @@ -891,7 +1100,7 @@ }, "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" }, @@ -904,6 +1113,15 @@ } ] }, + "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``.", @@ -962,7 +1180,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.\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)", + "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 : Optional[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": { @@ -991,7 +1209,7 @@ }, "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" }, @@ -1004,6 +1222,15 @@ } ] }, + "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``.", @@ -1074,7 +1301,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.\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)", + "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 : Optional[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": { @@ -1103,7 +1330,7 @@ }, "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" }, @@ -1116,6 +1343,15 @@ } ] }, + "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``.", @@ -1182,7 +1418,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.\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)", + "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 : Optional[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": { @@ -1211,7 +1447,7 @@ }, "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" }, @@ -1224,6 +1460,15 @@ } ] }, + "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``.", @@ -1290,7 +1535,7 @@ }, "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.\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]\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 : Optional[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]\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)", "type": "object", "properties": { "name": { @@ -1331,6 +1576,15 @@ } ] }, + "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``.", @@ -1473,7 +1727,7 @@ }, "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.\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.", + "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 : Optional[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": { @@ -1502,7 +1756,7 @@ }, "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" }, @@ -1515,6 +1769,15 @@ } ] }, + "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``.", @@ -1547,7 +1810,7 @@ }, "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.\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)", + "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 : Optional[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": { @@ -1576,7 +1839,7 @@ }, "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" }, @@ -1589,6 +1852,15 @@ } ] }, + "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``.", @@ -1730,7 +2002,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.\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.\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 : Optional[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": { @@ -1759,7 +2031,7 @@ }, "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" }, @@ -1772,6 +2044,15 @@ } ] }, + "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``.", @@ -1815,6 +2096,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.", @@ -1844,22 +2134,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.\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)", + "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 : Optional[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": { @@ -1888,7 +2169,7 @@ }, "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" }, @@ -1901,6 +2182,15 @@ } ] }, + "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``.", @@ -2010,7 +2300,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.\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)", + "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 : Optional[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": { @@ -2039,7 +2329,7 @@ }, "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" }, @@ -2052,6 +2342,15 @@ } ] }, + "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``.", @@ -2145,7 +2444,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.\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)", + "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 : Optional[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": { @@ -2174,7 +2473,7 @@ }, "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" }, @@ -2187,6 +2486,15 @@ } ] }, + "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``.", @@ -2310,7 +2618,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.\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)", + "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 : Optional[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": { @@ -2339,7 +2647,7 @@ }, "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" }, @@ -2352,6 +2660,15 @@ } ] }, + "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``.", @@ -2461,7 +2778,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.\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)", + "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 : Optional[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": { @@ -2490,7 +2807,7 @@ }, "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" }, @@ -2503,6 +2820,15 @@ } ] }, + "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``.", @@ -2612,7 +2938,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.\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)", + "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.\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": { @@ -2653,6 +2979,15 @@ } ] }, + "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``.", @@ -3172,7 +3507,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.\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... )", + "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 : Optional[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": { @@ -3215,7 +3550,7 @@ }, "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" }, @@ -3228,6 +3563,15 @@ } ] }, + "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``.", @@ -3287,7 +3631,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.\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... )", + "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 : Optional[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": { @@ -3330,7 +3674,7 @@ }, "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" }, @@ -3343,6 +3687,15 @@ } ] }, + "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``.", @@ -3922,6 +4275,7 @@ "propertyName": "type", "mapping": { "Box": "#/definitions/Box", + "Transformed": "#/definitions/Transformed", "ClipOperation": "#/definitions/ClipOperation", "GeometryGroup": "#/definitions/GeometryGroup", "Sphere": "#/definitions/Sphere", @@ -3935,6 +4289,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -3996,6 +4353,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -4026,6 +4386,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -4057,9 +4420,92 @@ ], "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" + }, + { + "$ref": "#/definitions/GeometryGroup" + }, + { + "$ref": "#/definitions/Sphere" + }, + { + "$ref": "#/definitions/Cylinder" + }, + { + "$ref": "#/definitions/PolySlab" + }, + { + "$ref": "#/definitions/ComplexPolySlabBase" + }, + { + "$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": [ + "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.\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]\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 : Optional[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]\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)", "type": "object", "properties": { "name": { @@ -4088,7 +4534,7 @@ }, "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" }, @@ -4101,6 +4547,15 @@ } ] }, + "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``.", @@ -4207,7 +4662,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": { @@ -4217,6 +4672,7 @@ "propertyName": "type", "mapping": { "Box": "#/definitions/Box", + "Transformed": "#/definitions/Transformed", "ClipOperation": "#/definitions/ClipOperation", "GeometryGroup": "#/definitions/GeometryGroup", "Sphere": "#/definitions/Sphere", @@ -4230,6 +4686,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, @@ -4575,9 +5034,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", @@ -4655,11 +5119,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.", @@ -4689,9 +5148,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[Literal[0], Literal[0], 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[Literal[0], Literal[0], 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", @@ -4780,11 +5244,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.", @@ -4813,9 +5272,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", @@ -4893,11 +5357,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.", @@ -4961,9 +5420,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", @@ -5041,11 +5505,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.", @@ -5259,9 +5718,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", @@ -5339,11 +5803,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.", @@ -5404,9 +5863,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", @@ -5484,11 +5948,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.", @@ -5629,9 +6088,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", @@ -5709,11 +6173,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.", @@ -5733,9 +6192,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", @@ -5813,11 +6277,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.", @@ -5843,9 +6302,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", @@ -5923,11 +6387,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.", @@ -6651,7 +7110,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 last point of the monitor grid is 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": { @@ -6742,7 +7201,8 @@ }, "colocate": { "title": "Colocate fields", - "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.", + "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", + "default": true, "type": "boolean" }, "freqs": { @@ -6810,7 +7270,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 last point of the monitor grid is 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": { @@ -6901,7 +7361,8 @@ }, "colocate": { "title": "Colocate fields", - "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.", + "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", + "default": true, "type": "boolean" }, "start": { @@ -7581,7 +8042,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 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).\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": { @@ -7678,7 +8139,7 @@ }, "colocate": { "title": "Colocate fields", - "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes). Default is ``True``.", + "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", "default": true, "type": "boolean" }, @@ -8753,7 +9214,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": { @@ -8763,6 +9224,7 @@ "propertyName": "type", "mapping": { "Box": "#/definitions/Box", + "Transformed": "#/definitions/Transformed", "ClipOperation": "#/definitions/ClipOperation", "GeometryGroup": "#/definitions/GeometryGroup", "Sphere": "#/definitions/Sphere", @@ -8776,6 +9238,9 @@ { "$ref": "#/definitions/Box" }, + { + "$ref": "#/definitions/Transformed" + }, { "$ref": "#/definitions/ClipOperation" }, From c2766f9d0bddca5c77c60f869634d1d417aaf0df Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 30 Oct 2023 16:21:08 -0400 Subject: [PATCH 27/83] fix typos in changelog --- CHANGELOG.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d58a11e..fe80374c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,7 +99,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. @@ -296,7 +296,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). @@ -395,7 +395,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. @@ -537,7 +537,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 @@ -625,7 +625,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. @@ -635,7 +635,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. @@ -677,7 +677,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. @@ -692,9 +692,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. @@ -704,7 +704,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 @@ -739,7 +739,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 @@ -799,7 +799,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. From 9e937408d1ae44ee60f634e611a8ad554a923a31 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 30 Oct 2023 16:41:33 -0400 Subject: [PATCH 28/83] new changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe80374c0..7de09b686 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ 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] + +### Added + +### Changed + +### Fixed + ## [2.5.0rc2] - 2023-10-30 ### Added From c32850744aa7c69906351496d32426316aef21bf Mon Sep 17 00:00:00 2001 From: Lucas Heitzmann Gabrielli Date: Mon, 30 Oct 2023 17:21:25 -0300 Subject: [PATCH 29/83] Avoid duplicate log messages in Jupyter When a rich console is created within a jupyter notebook, it autodetects the environment and outputs log messages by default, which ends up duplicating our messages, because we already have a standard output by default. A flag can be set to false to inhbit the autodetection. Signed-off-by: Lucas Heitzmann Gabrielli --- CHANGELOG.md | 1 + tidy3d/log.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7de09b686..c5647502f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed ### Fixed +- Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. ## [2.5.0rc2] - 2023-10-30 diff --git a/tidy3d/log.py b/tidy3d/log.py index f38f37912..c98e0c10f 100644 --- a/tidy3d/log.py +++ b/tidy3d/log.py @@ -384,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 From 07a27f243fe644b345c790ad7edd5c8e16567807 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Tue, 31 Oct 2023 11:35:00 -0400 Subject: [PATCH 30/83] truncate adjoint filter if input shape small --- CHANGELOG.md | 1 + tests/test_plugins/test_adjoint.py | 14 ++++++++++ tidy3d/plugins/adjoint/utils/filter.py | 37 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5647502f..605f4243b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - 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. ## [2.5.0rc2] - 2023-10-30 diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index a03cfbfa9..59eaf6b23 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -1473,6 +1473,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.""" 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") From 30f36938eaa70da87447e15b3e08d900ad97c814 Mon Sep 17 00:00:00 2001 From: Casey Wojcik Date: Wed, 1 Nov 2023 13:41:22 +0000 Subject: [PATCH 31/83] Fix bug in eps_model for integer frequency arrays --- tests/test_components/test_medium.py | 3 +++ tidy3d/components/medium.py | 1 + 2 files changed, 4 insertions(+) diff --git a/tests/test_components/test_medium.py b/tests/test_components/test_medium.py index 69cf7053c..e8b74f0a7 100644 --- a/tests/test_components/test_medium.py +++ b/tests/test_components/test_medium.py @@ -113,6 +113,9 @@ 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])) + def test_medium_dispersion_conversion(): diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index 464db9a83..f160bb1db 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -58,6 +58,7 @@ def _eps_model(self, frequency: float) -> complex: 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 From c5aec95f06e4e367e1a0f0ba20b524c8a4c5702b Mon Sep 17 00:00:00 2001 From: momchil Date: Tue, 31 Oct 2023 16:28:10 -0700 Subject: [PATCH 32/83] Setting internal json indent to None, and splitting json string in hdf5 --- CHANGELOG.md | 3 ++ tests/sims/simulation_2_5_0rc2.h5 | Bin 374808 -> 449568 bytes tests/test_components/test_IO.py | 18 +++++++--- tests/test_web/test_webapi.py | 2 +- tests/test_web/test_webapi_heat.py | 2 +- tidy3d/components/base.py | 40 +++++++++++++++++----- tidy3d/plugins/adjoint/components/base.py | 7 ++-- tidy3d/web/core/file_util.py | 22 ++++++++++-- tidy3d/web/core/task_core.py | 15 ++------ 9 files changed, 74 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 605f4243b..4e60db699 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Changed +- 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. ### Fixed - 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. + ## [2.5.0rc2] - 2023-10-30 diff --git a/tests/sims/simulation_2_5_0rc2.h5 b/tests/sims/simulation_2_5_0rc2.h5 index 6897bbaa28dd5de7b133f07842bd378d5fa94367..1e92efab402c2754e3c720a4c94749b0c33212f7 100644 GIT binary patch literal 449568 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 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 +379,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 +434,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 +522,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 +581,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.""" 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/web/core/file_util.py b/tidy3d/web/core/file_util.py index bd3bbdc9b..9305eaab1 100644 --- a/tidy3d/web/core/file_util.py +++ b/tidy3d/web/core/file_util.py @@ -49,12 +49,28 @@ def read_simulation_from_hdf5_gz(file_name: str) -> str: 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: - json_string = f_handle[JSON_TAG][()] - return json_string + 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: diff --git a/tidy3d/web/core/task_core.py b/tidy3d/web/core/task_core.py index a37533987..f44cff153 100644 --- a/tidy3d/web/core/task_core.py +++ b/tidy3d/web/core/task_core.py @@ -8,7 +8,6 @@ from typing import List, Optional, Callable, Tuple import pydantic.v1 as pd from pydantic.v1 import Extra, Field, parse_obj_as -import h5py from . import http_util from .core_config import get_logger_console @@ -22,16 +21,8 @@ from .types import Tidy3DResource -from .constants import SIM_FILE_HDF5_GZ, SIMULATION_DATA_HDF5, SIM_LOG_FILE, JSON_TAG -from .file_util import extract_gzip_file - - -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): @@ -313,7 +304,7 @@ def get_simulation_json(self, to_file: str, verbose: bool = True) -> pathlib.Pat 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")) From ebae633e946e0d3061c049afeee09d1c1189cdca Mon Sep 17 00:00:00 2001 From: Casey Wojcik Date: Wed, 6 Sep 2023 04:35:37 +0100 Subject: [PATCH 33/83] Added TwoPhotonAbsorption and KerrNonlinearity --- CHANGELOG.md | 3 + tests/sims/simulation_2_5_0rc2.json | 58 +++- tests/test_components/test_medium.py | 119 +++++++- tests/test_components/test_source.py | 35 ++- tests/utils.py | 16 ++ tidy3d/__init__.py | 7 +- tidy3d/components/medium.py | 399 +++++++++++++++++++++++---- tidy3d/components/simulation.py | 27 +- tidy3d/components/source.py | 12 + tidy3d/constants.py | 2 + 10 files changed, 597 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e60db699..b3e851eb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Added support for two-photon absorption via `TwoPhotonAbsorption` class. Added `KerrNonlinearity` that implements Kerr effect without third-harmonic generation. ### Changed - 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`. ### Fixed - Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. @@ -32,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### Fixed - If there are no adjoint sources for a simulation involved in an objective function, make a mock source with zero amplitude and warn user. diff --git a/tests/sims/simulation_2_5_0rc2.json b/tests/sims/simulation_2_5_0rc2.json index 70702aab5..127b7761a 100644 --- a/tests/sims/simulation_2_5_0rc2.json +++ b/tests/sims/simulation_2_5_0rc2.json @@ -675,9 +675,63 @@ "frequency_range": null, "allow_gain": false, "nonlinear_spec": { - "numiters": 20, "type": "NonlinearSusceptibility", - "chi3": 0.1 + "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, diff --git a/tests/test_components/test_medium.py b/tests/test_components/test_medium.py index e8b74f0a7..b0d561bb0 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 @@ -546,13 +546,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_source.py b/tests/test_components/test_source.py index c59a5868a..a756df266 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") @@ -268,7 +269,7 @@ def check_freq_grid(freq_grid, num_freqs): ) -def test_custom_source_time(): +def test_custom_source_time(log_capture): ts = np.linspace(0, 30, 1001) amp_time = ts / max(ts) @@ -276,31 +277,22 @@ def test_custom_source_time(): 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) - # test interpolation cst = td.CustomSourceTime.from_values(freq0=1, fwidth=0.1, values=np.linspace(0, 9, 10), dt=0.1) assert np.allclose( cst.amp_time(0.09), [0.9 * np.exp(-1j * 2 * np.pi * 0.09)], 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=[1], 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) source = td.PointDipole(center=(0, 0, 0), source_time=cst, polarization="Ex") sim = sim.updated_copy(sources=[source]) @@ -308,3 +300,20 @@ def test_custom_source_time(): assert np.allclose( cst.amp_time(sim.tmesh[1:]), np.exp(-1j * 2 * np.pi * sim.tmesh[1:]), 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=1, fwidth=0.1)) + 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=1, fwidth=0.1) + assert np.allclose(cst.amp_time([0]), [1], rtol=0, atol=ATOL) diff --git a/tests/utils.py b/tests/utils.py index 01bf66aa5..3f10be28f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -213,6 +213,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] diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index ce6f2f469..2aa463e01 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -16,7 +16,7 @@ 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 @@ -94,7 +94,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 @@ -187,7 +187,10 @@ def set_logging_level(level: str) -> None: "LinearChargePerturbation", "CustomChargePerturbation", "NonlinearSpec", + "NonlinearModel", "NonlinearSusceptibility", + "TwoPhotonAbsorption", + "KerrNonlinearity", "Structure", "MeshOverrideStructure", "ModeSpec", diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index f160bb1db..250dd3c28 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -14,7 +14,7 @@ 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 @@ -22,6 +22,7 @@ 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 HERTZ, CONDUCTIVITY, PERMITTIVITY, RADPERSEC, MICROMETER, SECOND +from ..constants import WATT, VOLT from ..exceptions import ValidationError, SetupError from ..log import log from .transformation import RotationType @@ -36,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. @@ -51,7 +53,6 @@ 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: @@ -83,54 +84,230 @@ 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 + + +class NonlinearSusceptibility(NonlinearModel): + """Model for an instantaneous nonlinear chi3 susceptibility. + The expression for the instantaneous nonlinear polarization is given below. - Note + 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 -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, []) + + +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 ---- @@ -141,15 +318,103 @@ 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, []) + + +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. -NonlinearSpecType = Union[NonlinearSusceptibility] + 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 + + @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): @@ -174,7 +439,7 @@ class AbstractMedium(ABC, Tidy3dBaseModel): "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.", @@ -186,15 +451,55 @@ class AbstractMedium(ABC, Tidy3dBaseModel): description="Modulation spec applied on top of the base medium properties.", ) - @pd.validator("nonlinear_spec", always=True) - def _validate_nonlinear_spec(cls, val): + @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.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, @@ -681,16 +986,6 @@ 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.""" @@ -810,16 +1105,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""" diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 8b3fbae01..ecb635187 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -26,7 +26,7 @@ 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 @@ -821,6 +821,21 @@ 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 """ @@ -830,6 +845,7 @@ def _post_init_validators(self) -> None: _ = 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.""" @@ -901,6 +917,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: diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index 8d669d8d5..ef91b1b99 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -168,6 +168,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) @@ -200,6 +205,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. diff --git a/tidy3d/constants.py b/tidy3d/constants.py index 96429096e..aeacf056e 100644 --- a/tidy3d/constants.py +++ b/tidy3d/constants.py @@ -47,6 +47,8 @@ KELVIN = "K" CMCUBE = "cm^3" PERCMCUBE = "1/cm^3" +WATT = "W" +VOLT = "V" THERMAL_CONDUCTIVITY = "W/(um*K)" SPECIFIC_HEAT_CAPACITY = "J/(kg*K)" From 73cf98ac760273f250a54e16c237f88c508a3804 Mon Sep 17 00:00:00 2001 From: momchil Date: Mon, 6 Nov 2023 14:39:30 -0800 Subject: [PATCH 34/83] bumping version to 2.5.0rc3 --- tidy3d/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/version.py b/tidy3d/version.py index cc231c249..ca985b250 100644 --- a/tidy3d/version.py +++ b/tidy3d/version.py @@ -1,3 +1,3 @@ """Defines the front end version of tidy3d""" -__version__ = "2.5.0rc2" +__version__ = "2.5.0rc3" From 4045f7bf17df62e3e1ab14f4cfdfeb4069c606e0 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 6 Nov 2023 17:23:37 -0500 Subject: [PATCH 35/83] use mode solver fields as default args in conversion methods --- CHANGELOG.md | 1 + tests/test_plugins/test_mode_solver.py | 53 ++++++++++++++++++++++++++ tidy3d/plugins/mode/mode_solver.py | 34 +++++++++++++---- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e851eb2..ea1e44e84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - 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. ### Fixed - Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 76fc74fd7..dc303cfe4 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -653,3 +653,56 @@ def test_mode_solver_nan_pol_fraction(): 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.0, fwidth=1.0) + + 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/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index b2425955c..686d27f07 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -573,7 +573,7 @@ def _grid_correction( 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 @@ -583,8 +583,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. @@ -595,6 +596,9 @@ def to_source( inputs. """ + if direction is None: + direction = self.direction + return ModeSource( center=self.plane.center, size=self.plane.size, @@ -604,7 +608,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. @@ -612,6 +616,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. @@ -622,6 +627,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, @@ -663,7 +677,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 @@ -674,8 +688,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. @@ -685,6 +700,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 ) @@ -694,8 +710,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 @@ -703,8 +719,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. @@ -714,6 +731,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) From 890761caedc52a9a1ea4be18d0075d0aa4f9aaaf Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Tue, 7 Nov 2023 14:31:07 -0500 Subject: [PATCH 36/83] proper equality checking between tidy3d base model --- CHANGELOG.md | 1 + tests/sims/simulation_2_5_0rc3.h5 | Bin 0 -> 451528 bytes tests/sims/simulation_2_5_0rc3.json | 2153 ++++++++++++++++++++++++++ tests/test_components/test_IO.py | 12 +- tests/test_components/test_base.py | 9 + tidy3d/components/base.py | 44 +- tidy3d/components/data/data_array.py | 4 + 7 files changed, 2218 insertions(+), 5 deletions(-) create mode 100644 tests/sims/simulation_2_5_0rc3.h5 create mode 100644 tests/sims/simulation_2_5_0rc3.json diff --git a/CHANGELOG.md b/CHANGELOG.md index ea1e44e84..a9d63cbdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ## [2.5.0rc2] - 2023-10-30 diff --git a/tests/sims/simulation_2_5_0rc3.h5 b/tests/sims/simulation_2_5_0rc3.h5 new file mode 100644 index 0000000000000000000000000000000000000000..a5cf8c5443fb7cb8548b09aea8833b0762ff5a24 GIT binary patch literal 451528 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 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/data/data_array.py b/tidy3d/components/data/data_array.py index a1e9274c2..b0282c1c4 100644 --- a/tidy3d/components/data/data_array.py +++ b/tidy3d/components/data/data_array.py @@ -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(): From 6bd33dd82e4d6643cb521e6ff6989edc493cd79b Mon Sep 17 00:00:00 2001 From: momchil Date: Fri, 3 Nov 2023 15:27:41 -0700 Subject: [PATCH 37/83] Split ModeSolver.data_raw into smaller methods --- tidy3d/plugins/mode/mode_solver.py | 160 ++++++++++++++++------------- 1 file changed, 90 insertions(+), 70 deletions(-) diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 686d27f07..87103466d 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -191,6 +191,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. @@ -204,6 +210,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 ) @@ -223,15 +253,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( @@ -253,7 +277,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( @@ -267,72 +291,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) + + # 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], - np.where(np.isnan(te_frac))[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], - 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], + 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: From b7a7d7ac44b8fb68bee24b47ab2dac4d228253fe Mon Sep 17 00:00:00 2001 From: momchil Date: Thu, 9 Nov 2023 09:50:22 -0800 Subject: [PATCH 38/83] Changelog item regarding sign change in some cases with field sources --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9d63cbdc..a0e1870e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### Fixed - Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. From f7edadad9a38d5624c7bbf9edc9c5d363ed53a17 Mon Sep 17 00:00:00 2001 From: Casey Wojcik Date: Fri, 6 Oct 2023 12:56:35 +0100 Subject: [PATCH 39/83] Added PECMedium to IsotropicUniformMediumType, and added PEC2D --- CHANGELOG.md | 2 + tests/test_components/test_simulation.py | 25 +++++++++--- tests/utils.py | 8 ++++ tidy3d/__init__.py | 4 +- tidy3d/components/grid/mesher.py | 10 ++++- tidy3d/components/medium.py | 51 ++++++++++++++++++++++-- tidy3d/components/scene.py | 8 ++-- tidy3d/components/simulation.py | 15 +++++-- 8 files changed, 103 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e1870e9..89af8c224 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Added support for two-photon absorption via `TwoPhotonAbsorption` class. Added `KerrNonlinearity` that implements Kerr effect without third-harmonic generation. +- Support for an anisotropic medium containing PEC components. + ### Changed - 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`. diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index d0a99b6fc..3fc4de86a 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -1712,7 +1712,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]), @@ -1743,11 +1743,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() diff --git a/tests/utils.py b/tests/utils.py index 3f10be28f..6c82bf43e 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -139,6 +139,14 @@ 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, diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 2aa463e01..c33d9d022 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -11,7 +11,8 @@ 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 @@ -166,6 +167,7 @@ def set_logging_level(level: str) -> None: "PEC", "PECMedium", "Medium2D", + "PEC2D", "Sellmeier", "Debye", "Drude", 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/medium.py b/tidy3d/components/medium.py index 250dd3c28..5378e3310 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -20,7 +20,7 @@ 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 @@ -479,6 +479,13 @@ def _post_init_validators(self) -> None: def _validate_nonlinear_spec(self): """Check compatibility with nonlinear_spec.""" + 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): @@ -781,6 +788,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.""" @@ -960,6 +972,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") @@ -3161,7 +3178,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, @@ -3318,6 +3335,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 @@ -4085,7 +4111,7 @@ def _validate_modulation_spec(cls, 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 = [] @@ -4097,7 +4123,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] @@ -4228,6 +4255,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 @@ -4390,6 +4419,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/scene.py b/tidy3d/components/scene.py index 2bb5cc503..55a2efec6 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -14,7 +14,7 @@ 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, PECMedium +from .medium import Medium, MediumType from .medium import AbstractCustomMedium, Medium2D, MediumType3D from .medium import AbstractPerturbationMedium from .grid.grid import Grid @@ -413,7 +413,7 @@ def _get_structure_plot_params(self, mat_index: int, medium: Medium) -> PlotPara if mat_index == 0 or medium == self.medium: # background medium plot_params = plot_params.copy(update={"facecolor": "white", "edgecolor": "white"}) - elif isinstance(medium, PECMedium): + elif medium.is_pec: # perfect electrical conductor plot_params = plot_params.copy( update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1} @@ -838,7 +838,7 @@ def eps_bounds(self, freq: float = None) -> Tuple[float, float]: """ medium_list = [self.medium] + list(self.mediums) - medium_list = [medium for medium in medium_list if not isinstance(medium, PECMedium)] + medium_list = [medium for medium in medium_list if not medium.is_pec] # regular medium eps_list = [ medium.eps_model(freq).real @@ -983,7 +983,7 @@ def _get_structure_eps_plot_params( if alpha is not None: plot_params = plot_params.copy(update={"alpha": alpha}) - if isinstance(medium, PECMedium): + if medium.is_pec: # perfect electrical conductor plot_params = plot_params.copy( update={"facecolor": "gold", "edgecolor": "k", "linewidth": 1} diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index ecb635187..d663f808e 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -19,7 +19,7 @@ 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 @@ -723,7 +723,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): @@ -731,9 +731,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 From 3913bce9b9c4a4d1c42b376e4110f080a5df9226 Mon Sep 17 00:00:00 2001 From: Casey Wojcik Date: Wed, 8 Nov 2023 16:34:10 +0000 Subject: [PATCH 40/83] Added LO-TO form of PoleResidue model --- CHANGELOG.md | 1 + tests/test_components/test_medium.py | 8 ++ tidy3d/components/medium.py | 105 +++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89af8c224..0037b5ab6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 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`. - Support for an anisotropic medium containing PEC components. diff --git a/tests/test_components/test_medium.py b/tests/test_components/test_medium.py index b0d561bb0..29d8f1966 100644 --- a/tests/test_components/test_medium.py +++ b/tests/test_components/test_medium.py @@ -116,6 +116,14 @@ def test_medium_dispersion(): # 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(): diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index 5378e3310..2779c4093 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -2062,6 +2062,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. From 52e48ea74a6c60ce5de87cf30897805550af71ba Mon Sep 17 00:00:00 2001 From: Lucas Heitzmann Gabrielli Date: Mon, 13 Nov 2023 08:50:54 -0300 Subject: [PATCH 41/83] Correctly handle 2D/1D custom medium when exporting to GDSII The 0D case is also handled just to avoid an obscure error. Signed-off-by: Lucas Heitzmann Gabrielli --- CHANGELOG.md | 1 + tests/test_components/test_structure.py | 22 ++++++++++++++++++++++ tidy3d/components/structure.py | 10 ++++++++-- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0037b5ab6..fcfcdb578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ## [2.5.0rc2] - 2023-10-30 diff --git a/tests/test_components/test_structure.py b/tests/test_components/test_structure.py index d556fdde9..45f6e2a73 100644 --- a/tests/test_components/test_structure.py +++ b/tests/test_components/test_structure.py @@ -50,6 +50,28 @@ def test_custom_medium_to_gds(tmp_path): 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)) diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index 17ba36e49..6815db054 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -197,11 +197,17 @@ def to_gdstk( 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 = min(np.diff(eps.x).min(), np.diff(eps.y).min(), np.diff(eps.z).min()) + 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()) - bb_min, bb_max = self.geometry.bounds 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, From 96c1f656ed63d22e45315ba8325a4067a63781c1 Mon Sep 17 00:00:00 2001 From: momchil Date: Mon, 13 Nov 2023 13:35:38 -0800 Subject: [PATCH 42/83] SourceTime.plot_spectrum missing return after refactor --- tidy3d/components/source.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index ef91b1b99..b2cc9f6e2 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -71,7 +71,7 @@ def plot_spectrum( """ fmin, fmax = self.frequency_range() - self.plot_spectrum_in_frequency_range( + return self.plot_spectrum_in_frequency_range( times, fmin, fmax, num_freqs=num_freqs, val=val, ax=ax, complex_fields=complex_fields ) From 0a52c995cd1469dc7aa7e06acb3332ff0435b807 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 13 Nov 2023 13:39:58 -0500 Subject: [PATCH 43/83] add SimulationData.mnt_data_from_file() convenience method --- CHANGELOG.md | 1 + tests/test_components/test_IO.py | 38 +++++++++++++++++ tidy3d/components/data/sim_data.py | 66 ++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcfcdb578..9228fde8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Can create `PoleResidue` from LO-TO form via `PoleResidue.from_lo_to`. - 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. ### Changed - 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. diff --git a/tests/test_components/test_IO.py b/tests/test_components/test_IO.py index 5d3dfe23d..b0b4ba623 100644 --- a/tests/test_components/test_IO.py +++ b/tests/test_components/test_IO.py @@ -15,6 +15,8 @@ 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 @@ -278,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/tidy3d/components/data/sim_data.py b/tidy3d/components/data/sim_data.py index f52fe2d99..f5345bfcf 100644 --- a/tidy3d/components/data/sim_data.py +++ b/tidy3d/components/data/sim_data.py @@ -2,9 +2,12 @@ from __future__ import annotations from typing import Callable, Tuple +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 ..simulation import Simulation @@ -14,12 +17,16 @@ from ..viz import equal_aspect, add_ax_if_none 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(AbstractSimulationData): """Stores data from a collection of :class:`.Monitor` objects in a :class:`.Simulation`. @@ -369,6 +376,65 @@ 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.") + def plot_field( self, field_monitor_name: str, From 7cb20270bf42252e0ed5b375eaf698d2cd84c6db Mon Sep 17 00:00:00 2001 From: momchil Date: Tue, 14 Nov 2023 13:23:22 -0800 Subject: [PATCH 44/83] Removing special direction='-' handling in adjoint sources after solver fix --- tidy3d/plugins/adjoint/components/data/monitor_data.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tidy3d/plugins/adjoint/components/data/monitor_data.py b/tidy3d/plugins/adjoint/components/data/monitor_data.py index eb620c9a4..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)) From 77f8076e2a325b44568aa912a57e67a2a16d8c77 Mon Sep 17 00:00:00 2001 From: qingeng Date: Sat, 11 Nov 2023 09:49:55 +0800 Subject: [PATCH 45/83] better error message when user try to download data that doesn't exist. --- CHANGELOG.md | 2 +- tidy3d/web/api/mode.py | 19 ++++++++++++------- tidy3d/web/core/task_core.py | 19 ++++++++++++------- 3 files changed, 25 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9228fde8a..c489948ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ## [2.5.0rc2] - 2023-10-30 diff --git a/tidy3d/web/api/mode.py b/tidy3d/web/api/mode.py index 57f7dc43c..deba13191 100644 --- a/tidy3d/web/api/mode.py +++ b/tidy3d/web/api/mode.py @@ -423,13 +423,18 @@ def get_result( :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, - ) + 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)} diff --git a/tidy3d/web/core/task_core.py b/tidy3d/web/core/task_core.py index f44cff153..0c2dd8cbd 100644 --- a/tidy3d/web/core/task_core.py +++ b/tidy3d/web/core/task_core.py @@ -472,13 +472,18 @@ def get_sim_data_hdf5( if not self.task_id: 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 From 2828687249ea3c0290cc08b2fb4ff3b379021128 Mon Sep 17 00:00:00 2001 From: Lucas Heitzmann Gabrielli Date: Wed, 15 Nov 2023 12:39:56 -0300 Subject: [PATCH 46/83] Fix for file names containing dots Signed-off-by: Lucas Heitzmann Gabrielli --- CHANGELOG.md | 1 + tidy3d/components/base.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c489948ee..2a41d9058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ## [2.5.0rc2] - 2023-10-30 diff --git a/tidy3d/components/base.py b/tidy3d/components/base.py index 120365548..98d0d3f54 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -68,13 +68,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): From 1b5b46c7a7f97c37dc3f71455a8fef9c293e140d Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 20 Nov 2023 12:37:15 -0500 Subject: [PATCH 47/83] matplotlib >= 3.5 --- CHANGELOG.md | 1 + requirements/basic.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a41d9058..ba388b68f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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`. ## [2.5.0rc2] - 2023-10-30 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 From af54303d7e0239ac0781fd2bd34a93fbef9f1558 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 20 Nov 2023 12:08:32 -0500 Subject: [PATCH 48/83] add '_hash_self' method for deterministic hash, fixes 'ComponentModeler._batch_file' inconsistency. --- CHANGELOG.md | 3 ++- tests/test_plugins/test_component_modeler.py | 5 +++++ tidy3d/components/base.py | 8 ++++++++ tidy3d/plugins/smatrix/smatrix.py | 3 ++- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba388b68f..ed86f8b77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 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`. - - 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. ### Changed - 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. @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ## [2.5.0rc2] - 2023-10-30 diff --git a/tests/test_plugins/test_component_modeler.py b/tests/test_plugins/test_component_modeler.py index 39f863136..cae28d6da 100644 --- a/tests/test_plugins/test_component_modeler.py +++ b/tests/test_plugins/test_component_modeler.py @@ -369,3 +369,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/tidy3d/components/base.py b/tidy3d/components/base.py index 98d0d3f54..6a61839a9 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -8,6 +8,8 @@ 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 @@ -99,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() diff --git a/tidy3d/plugins/smatrix/smatrix.py b/tidy3d/plugins/smatrix/smatrix.py index 85a67108c..e2d789285 100644 --- a/tidy3d/plugins/smatrix/smatrix.py +++ b/tidy3d/plugins/smatrix/smatrix.py @@ -349,7 +349,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.""" From be8d5e73fd49164e808a883c5e12ae0fa97c7810 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Wed, 22 Nov 2023 07:44:49 -0500 Subject: [PATCH 49/83] add ComponentModeler.plot_sim_eps() --- CHANGELOG.md | 2 ++ tests/test_plugins/test_component_modeler.py | 6 ++++++ tidy3d/plugins/smatrix/smatrix.py | 20 ++++++++++++++++++-- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed86f8b77..8243497fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### Changed - 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. @@ -28,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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()`. ## [2.5.0rc2] - 2023-10-30 diff --git a/tests/test_plugins/test_component_modeler.py b/tests/test_plugins/test_component_modeler.py index cae28d6da..c65a74514 100644 --- a/tests/test_plugins/test_component_modeler.py +++ b/tests/test_plugins/test_component_modeler.py @@ -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)) diff --git a/tidy3d/plugins/smatrix/smatrix.py b/tidy3d/plugins/smatrix/smatrix.py index e2d789285..ad98aa2de 100644 --- a/tidy3d/plugins/smatrix/smatrix.py +++ b/tidy3d/plugins/smatrix/smatrix.py @@ -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: From 192c547b243016d0a98f9c7edfe36942f272c6df Mon Sep 17 00:00:00 2001 From: Shashwat Sharma Date: Fri, 17 Nov 2023 20:24:53 -0500 Subject: [PATCH 50/83] fix for 2D PEC --- CHANGELOG.md | 3 +++ tidy3d/components/simulation.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8243497fc..6c3bf8f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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. ### Changed - 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. @@ -30,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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()`. +- Fixed a couple bugs in the handling of 2D PEC materials. + ## [2.5.0rc2] - 2023-10-30 diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index d663f808e..921d735cb 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -2703,7 +2703,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( From 5cc45f74ac654dddfb8dc3525b19b538b17e11fe Mon Sep 17 00:00:00 2001 From: Shashwat Sharma Date: Wed, 22 Nov 2023 11:03:47 -0500 Subject: [PATCH 51/83] allow downsampling of near fields for field projection monitors --- CHANGELOG.md | 1 + tests/sims/simulation_2_5_0rc3.h5 | Bin 451528 -> 460416 bytes tests/sims/simulation_2_5_0rc3.json | 121 ++++++++++++++++-- .../test_components/test_field_projection.py | 33 ++++- tidy3d/components/monitor.py | 85 +++++++----- 5 files changed, 191 insertions(+), 49 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c3bf8f35..85d3fa5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `_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. ### Changed - 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. diff --git a/tests/sims/simulation_2_5_0rc3.h5 b/tests/sims/simulation_2_5_0rc3.h5 index a5cf8c5443fb7cb8548b09aea8833b0762ff5a24..6742fa208627f81d0cbdaf185745064b189f0f5d 100644 GIT binary patch delta 15136 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 delta 14552 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 diff --git a/tests/sims/simulation_2_5_0rc3.json b/tests/sims/simulation_2_5_0rc3.json index 527f89dda..f66fe9fcf 100644 --- a/tests/sims/simulation_2_5_0rc3.json +++ b/tests/sims/simulation_2_5_0rc3.json @@ -307,6 +307,107 @@ } } }, + { + "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", @@ -910,14 +1011,14 @@ }, "transform": [ [ - 0.9659258262890683, - -0.25881904510252074, + 0.9659258262890684, + -0.2588190451025208, 0.0, 0.0 ], [ - 0.25881904510252074, - 0.9659258262890683, + 0.2588190451025208, + 0.9659258262890684, 0.0, 0.0 ], @@ -1405,7 +1506,7 @@ 1, 1 ], - "colocate": true, + "colocate": 1, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -1437,7 +1538,7 @@ 1, 1 ], - "colocate": true, + "colocate": 1, "start": 0.0, "stop": null, "interval": 1, @@ -1586,7 +1687,7 @@ 1, 1 ], - "colocate": true, + "colocate": 1, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1731,7 +1832,7 @@ 1, 1 ], - "colocate": true, + "colocate": 1, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1783,7 +1884,7 @@ 1, 1 ], - "colocate": true, + "colocate": 1, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1832,7 +1933,7 @@ 1, 1 ], - "colocate": true, + "colocate": 1, "freqs": [ 250000000000000.0, 300000000000000.0 diff --git a/tests/test_components/test_field_projection.py b/tests/test_components/test_field_projection.py index 20119be42..36436e955 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(): @@ -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/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 59b0d72bf..42c32331c 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -30,16 +30,16 @@ class Monitor(AbstractMonitor): 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.", @@ -103,14 +103,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, @@ -118,7 +118,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. " @@ -208,15 +208,15 @@ 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( True, - title="Colocate fields", + title="Colocate Fields", description="Toggle whether fields should be colocated to grid cell boundaries (i.e. " "primal grid nodes).", ) @@ -369,7 +369,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.", ) @@ -378,10 +378,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( @@ -402,7 +402,7 @@ 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.", @@ -410,7 +410,7 @@ class SurfaceIntegrationMonitor(Monitor, ABC): exclude_surfaces: Tuple[BoxSurface, ...] = pydantic.Field( None, - title="Excluded surfaces", + title="Excluded Surfaces", description="Surfaces to exclude in the integration, if a volume monitor.", ) @@ -527,7 +527,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.", @@ -556,14 +556,14 @@ 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).", ) @@ -579,13 +579,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 '-'.", ) @@ -612,7 +612,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, @@ -620,7 +620,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 " @@ -628,6 +628,21 @@ 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.", + ) + @property def projection_surfaces(self) -> Tuple[FieldProjectionSurface, ...]: """Surfaces of the monitor where near fields will be recorded for subsequent projection.""" @@ -685,14 +700,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, @@ -700,7 +715,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, @@ -748,13 +763,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, @@ -762,7 +777,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. " @@ -772,7 +787,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. " @@ -821,13 +836,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, ) @@ -886,7 +901,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.", @@ -894,7 +909,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.", From 4f8ace24936e59d741a2d84b7cd818ff1abd7380 Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Wed, 22 Nov 2023 10:14:56 -0500 Subject: [PATCH 52/83] add FieldData.apply_phase and phase kwarg to SimulationData.plot_field --- CHANGELOG.md | 2 ++ tests/test_data/test_monitor_data.py | 17 +++++++++++++++ tests/test_data/test_sim_data.py | 21 ++++++++++++------- tidy3d/components/data/dataset.py | 19 +++++++++++++++++ tidy3d/components/data/sim_data.py | 31 ++++++++++++++++++++++++---- 5 files changed, 79 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85d3fa5ac..12608aec8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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. ### Changed - 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. 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/tidy3d/components/data/dataset.py b/tidy3d/components/data/dataset.py index 81a0ff5fa..801207bed 100644 --- a/tidy3d/components/data/dataset.py +++ b/tidy3d/components/data/dataset.py @@ -32,6 +32,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 +276,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. diff --git a/tidy3d/components/data/sim_data.py b/tidy3d/components/data/sim_data.py index f5345bfcf..83abfd669 100644 --- a/tidy3d/components/data/sim_data.py +++ b/tidy3d/components/data/sim_data.py @@ -1,6 +1,6 @@ """ Simulation Level Data """ from __future__ import annotations -from typing import Callable, Tuple +from typing import Callable, Tuple, Union import pathlib import xarray as xr @@ -290,7 +290,9 @@ def get_poynting_vector(self, field_monitor_name: str) -> xr.Dataset: return xr.Dataset(poynting_components) - 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 @@ -301,6 +303,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 ------- @@ -320,6 +324,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"] @@ -435,6 +441,19 @@ def mnt_data_from_file(cls, fname: str, mnt_name: str, **parse_obj_kwargs) -> Mo 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, @@ -442,6 +461,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, @@ -466,6 +486,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 @@ -492,7 +515,6 @@ def plot_field( """ # get the DataArray corresponding to the monitor_name and field_name - # deprecated intensity if field_name == "int": log.warning( @@ -504,7 +526,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) @@ -512,6 +534,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": From 708efcac9c6685c046e0c846e6cf6d7634768d0a Mon Sep 17 00:00:00 2001 From: momchil Date: Tue, 21 Nov 2023 13:26:18 -0800 Subject: [PATCH 53/83] Ensuring mode solver fields are returned in requested precision --- CHANGELOG.md | 3 +-- tests/test_plugins/test_mode_solver.py | 14 +++++++++++++- tidy3d/components/data/monitor_data.py | 3 ++- tidy3d/components/monitor.py | 5 ++++- tidy3d/plugins/mode/mode_solver.py | 2 +- tidy3d/plugins/mode/solver.py | 4 ++++ 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12608aec8..37b785cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,8 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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()`. -- Fixed a couple bugs in the handling of 2D PEC materials. - +- Ensure that mode solver fields are returned in single precision if `ModeSolver.ModeSpec.precision == "single"`. ## [2.5.0rc2] - 2023-10-30 diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index dc303cfe4..c161fe52e 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -162,7 +162,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(): @@ -203,6 +203,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.""" @@ -295,6 +304,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: @@ -458,6 +468,7 @@ def test_mode_solver_angle_bend(): ) compare_colocation(ms) verify_pol_fraction(ms) + verify_dtype(ms) dataframe = ms.data.to_dataframe() # Plot field @@ -493,6 +504,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( diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index 71e966273..f7f28557e 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -1121,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 diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 42c32331c..a88f5a4fe 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -570,7 +570,10 @@ class ModeSolverMonitor(AbstractModeMonitor): 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): diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 87103466d..602f37e51 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -310,7 +310,7 @@ def _colocate_data(self, mode_solver_data: ModeSolverData) -> ModeSolverData: # 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) + 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) 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 From a4d3e11e2e80d28984da894abfbeafeb9dab8e0b Mon Sep 17 00:00:00 2001 From: momchil Date: Wed, 22 Nov 2023 15:23:08 -0800 Subject: [PATCH 54/83] Enforcing limits on the internal memory needed by fdtd monitors and by mode solver --- CHANGELOG.md | 2 ++ tests/test_components/test_simulation.py | 29 +++++++++++++++++-- tests/test_plugins/test_mode_solver.py | 12 ++++++++ tidy3d/components/monitor.py | 22 +++++++++++++- tidy3d/components/simulation.py | 37 +++++++++++++++++------- tidy3d/plugins/mode/mode_solver.py | 21 ++++++++++++-- tidy3d/web/api/mode.py | 1 + 7 files changed, 109 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37b785cbd..8afc02454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### Fixed - Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 3fc4de86a..c66eaa7c6 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -1606,7 +1606,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), @@ -1631,7 +1631,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), @@ -1649,6 +1649,31 @@ 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=[1], name="test", mode_spec=td.ModeSpec()), + td.ModeSolverMonitor(size=mnt_size, freqs=[1], name="test", mode_spec=td.ModeSpec()), + td.FluxMonitor(size=mnt_size, freqs=[1], name="test"), + td.FluxTimeMonitor(size=mnt_size, name="test"), + td.DiffractionMonitor(size=mnt_size, freqs=[1], name="test"), + td.FieldProjectionAngleMonitor(size=mnt_size, freqs=[1], 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( diff --git a/tests/test_plugins/test_mode_solver.py b/tests/test_plugins/test_mode_solver.py index c161fe52e..45c3d2b2d 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -11,6 +11,7 @@ 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.core.environment import Env @@ -243,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): diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index a88f5a4fe..074c65e03 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -1,5 +1,5 @@ """Objects that define how data is recorded from simulation.""" -from abc import ABC +from abc import ABC, abstractmethod from typing import Union, Tuple import pydantic.v1 as pydantic @@ -45,6 +45,14 @@ class Monitor(AbstractMonitor): "and is hard-coded for other monitors depending on their specific function.", ) + @abstractmethod + def storage_size(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Size of monitor storage given the number of points after discretization.""" + + 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): """:class:`Monitor` that records data in the frequency-domain.""" @@ -303,6 +311,11 @@ 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 + return BYTES_COMPLEX * num_cells * len(self.freqs) * self.mode_spec.num_modes * 6 + class FieldMonitor(AbstractFieldMonitor, FreqMonitor): """:class:`Monitor` that records electromagnetic fields in the frequency domain. @@ -454,6 +467,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.""" diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 921d735cb..28a0a48e2 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -72,6 +72,7 @@ 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 @@ -992,7 +993,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. " @@ -1009,6 +1010,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.""" @@ -1049,19 +1065,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(num_cells) + 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): diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 602f37e51..7f6508741 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -23,7 +23,7 @@ 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 ...exceptions import ValidationError, SetupError from ...constants import C_0 from .solver import compute_modes @@ -36,6 +36,9 @@ # 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 @@ -845,5 +848,19 @@ def plot_field( **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): - pass + self._validate_modes_size() diff --git a/tidy3d/web/api/mode.py b/tidy3d/web/api/mode.py index deba13191..16f28c172 100644 --- a/tidy3d/web/api/mode.py +++ b/tidy3d/web/api/mode.py @@ -183,6 +183,7 @@ def create( """ 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, From 99248a8369a312c0f985348b9eef9b33c0770f0e Mon Sep 17 00:00:00 2001 From: momchil Date: Mon, 27 Nov 2023 13:14:28 -0800 Subject: [PATCH 55/83] Avoid overflow in storage size estimate on 32-bit numpy builds --- tidy3d/components/simulation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 28a0a48e2..6204df2fa 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -1077,7 +1077,7 @@ def _monitor_num_cells(self, monitor: Monitor) -> int: num_cells = self.discretize_monitor(monitor).num_cells # take monitor downsampling into account num_cells = monitor.downsampled_num_cells(num_cells) - return np.prod(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.""" From b07b167726a08492758c6e439cee79b2cfc966db Mon Sep 17 00:00:00 2001 From: momchil Date: Thu, 30 Nov 2023 09:08:48 -0800 Subject: [PATCH 56/83] schema and changelog for 2.5.0rc3 --- CHANGELOG.md | 5 +- tidy3d/schema.json | 870 +++++++++++++++++++++++++++++---------------- 2 files changed, 562 insertions(+), 313 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8afc02454..c588d8acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.5.0rc3] - 2023-11-30 + ### Added - 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`. @@ -1039,7 +1041,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.5.0rc2...pre/2.5 +[Unreleased]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc3...pre/2.5 +[2.5.0rc3]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc2...v2.5.0rc3 [2.5.0rc2]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc1...v2.5.0rc2 [2.5.0rc1]: https://github.com/flexcompute/tidy3d/compare/v2.4.2...v2.5.0rc1 [Unreleased]: https://github.com/flexcompute/tidy3d/compare/v2.4.3...develop diff --git a/tidy3d/schema.json b/tidy3d/schema.json index fdbe241ed..4581a0d10 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.\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.0rc2\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... )", + "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.0rc3\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": { @@ -475,7 +475,7 @@ "version": { "title": "Version", "description": "String specifying the front end version number.", - "default": "2.5.0rc2", + "default": "2.5.0rc3", "type": "string" }, "run_time": { @@ -522,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", @@ -543,13 +536,226 @@ "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": { @@ -794,7 +1000,7 @@ }, "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 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 : Optional[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)", + "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": { @@ -830,7 +1036,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -890,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 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 : Optional[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)", + "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": { @@ -947,7 +1137,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -1071,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 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 : Optional[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)", + "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": { @@ -1107,7 +1300,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -1180,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 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 : Optional[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)", + "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": { @@ -1216,7 +1412,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -1301,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 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 : Optional[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)", + "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": { @@ -1337,7 +1536,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -1389,10 +1591,130 @@ }, "coeffs": { "title": "Coefficients", - "description": "List of (:math:`\\Delta\\epsilon_i, \\tau_i`) values for model.", + "description": "List of (:math:`\\Delta\\epsilon_i, \\tau_i`) values for model.", + "units": [ + "None (relative permittivity)", + "sec" + ], + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": [ + { + "type": "number" + }, + { + "type": "number", + "exclusiveMinimum": 0 + } + ] + } + } + }, + "required": [ + "coeffs" + ], + "additionalProperties": false + }, + "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 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": { + "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": "Drude", + "enum": [ + "Drude" + ], + "type": "string" + }, + "eps_inf": { + "title": "Epsilon at Infinity", + "description": "Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).", + "default": 1.0, + "units": "None (relative permittivity)", + "exclusiveMinimum": 0, + "type": "number" + }, + "coeffs": { + "title": "Coefficients", + "description": "List of (:math:`f_i, \\delta_i`) values for model.", "units": [ - "None (relative permittivity)", - "sec" + "Hz", + "Hz" ], "type": "array", "items": { @@ -1416,9 +1738,9 @@ ], "additionalProperties": false }, - "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 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 : Optional[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)", + "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": { @@ -1454,7 +1776,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -1490,52 +1815,18 @@ }, "type": { "title": "Type", - "default": "Drude", + "default": "PECMedium", "enum": [ - "Drude" + "PECMedium" ], "type": "string" - }, - "eps_inf": { - "title": "Epsilon at Infinity", - "description": "Relative permittivity at infinite frequency (:math:`\\epsilon_\\infty`).", - "default": 1.0, - "units": "None (relative permittivity)", - "exclusiveMinimum": 0, - "type": "number" - }, - "coeffs": { - "title": "Coefficients", - "description": "List of (:math:`f_i, \\delta_i`) values for model.", - "units": [ - "Hz", - "Hz" - ], - "type": "array", - "items": { - "type": "array", - "minItems": 2, - "maxItems": 2, - "items": [ - { - "type": "number" - }, - { - "type": "number", - "exclusiveMinimum": 0 - } - ] - } } }, - "required": [ - "coeffs" - ], "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.\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]\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": { @@ -1570,7 +1861,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -1623,7 +1917,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -1644,6 +1939,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] }, @@ -1658,7 +1956,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -1679,6 +1978,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] }, @@ -1693,7 +1995,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -1714,6 +2017,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] } @@ -1725,92 +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 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 : Optional[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.", - "allOf": [ - { - "$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 - }, "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 : Optional[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)", + "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": { @@ -1846,7 +2069,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -2002,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 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 : Optional[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)", + "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": { @@ -2038,7 +2264,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -2140,7 +2369,7 @@ }, "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 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 : Optional[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)", + "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": { @@ -2176,7 +2405,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -2300,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 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 : Optional[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)", + "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": { @@ -2336,7 +2568,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -2444,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 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 : Optional[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)", + "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": { @@ -2480,7 +2715,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -2618,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 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 : Optional[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)", + "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": { @@ -2654,7 +2892,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -2778,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 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 : Optional[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)", + "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": { @@ -2814,7 +3055,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -2938,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.\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)", + "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": { @@ -2973,7 +3217,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -3507,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 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 : Optional[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... )", + "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": { @@ -3557,7 +3804,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -3631,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 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 : Optional[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... )", + "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": { @@ -3681,7 +3931,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -4505,7 +4758,7 @@ }, "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 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 : Optional[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]\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": { @@ -4541,7 +4794,10 @@ "nonlinear_spec": { "title": "Nonlinear Spec", "description": "Nonlinear spec applied on top of the base medium properties.", - "allOf": [ + "anyOf": [ + { + "$ref": "#/definitions/NonlinearSpec" + }, { "$ref": "#/definitions/NonlinearSusceptibility" } @@ -4594,7 +4850,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -4615,6 +4872,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] }, @@ -4629,7 +4889,8 @@ "Sellmeier": "#/definitions/Sellmeier", "Lorentz": "#/definitions/Lorentz", "Debye": "#/definitions/Debye", - "Drude": "#/definitions/Drude" + "Drude": "#/definitions/Drude", + "PECMedium": "#/definitions/PECMedium" } }, "oneOf": [ @@ -4650,6 +4911,9 @@ }, { "$ref": "#/definitions/Drude" + }, + { + "$ref": "#/definitions/PECMedium" } ] } @@ -4882,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": { @@ -4970,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": { @@ -5148,7 +5412,7 @@ }, "PointDipole": { "title": "PointDipole", - "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[Literal[0], Literal[0], 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')", + "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": { @@ -7110,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 : 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)", + "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": { @@ -7174,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, @@ -7200,7 +7464,7 @@ ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", "default": true, "type": "boolean" @@ -7270,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 : 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')", + "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": { @@ -7334,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, @@ -7360,13 +7624,13 @@ ] }, "colocate": { - "title": "Colocate fields", + "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", @@ -7374,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" @@ -7419,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": { @@ -7483,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, @@ -7509,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": [ @@ -7558,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": { @@ -7622,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, @@ -7654,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": [ @@ -7694,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": [ "+", @@ -7703,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": { @@ -7728,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": { @@ -7792,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, @@ -7824,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": [ @@ -7833,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", @@ -7841,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": [ "+", @@ -7863,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": { @@ -7887,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": { @@ -7951,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, @@ -7983,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": [ @@ -8042,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).\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": { @@ -8106,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, @@ -8138,7 +8402,7 @@ ] }, "colocate": { - "title": "Colocate fields", + "title": "Colocate Fields", "description": "Toggle whether fields should be colocated to grid cell boundaries (i.e. primal grid nodes).", "default": true, "type": "boolean" @@ -8184,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": [ @@ -8204,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.\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": { @@ -8268,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, @@ -8280,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": [ @@ -8340,7 +8598,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": [ "+", @@ -8349,7 +8607,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": { @@ -8365,7 +8623,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", @@ -8384,20 +8642,20 @@ ] }, "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" }, "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": [ @@ -8413,7 +8671,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": [ @@ -8440,7 +8698,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.\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": { @@ -8504,8 +8762,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, @@ -8516,27 +8774,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": [ @@ -8576,7 +8828,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": [ "+", @@ -8585,7 +8837,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": { @@ -8601,7 +8853,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", @@ -8620,13 +8872,13 @@ ] }, "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" }, "proj_axis": { - "title": "Projection plane axis", + "title": "Projection Plane Axis", "description": "Axis along which the observation plane is oriented.", "enum": [ 0, @@ -8636,14 +8888,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": [ @@ -8659,7 +8911,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": [ @@ -8687,7 +8939,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.\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": { @@ -8751,8 +9003,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, @@ -8763,27 +9015,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": [ @@ -8823,7 +9069,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": [ "+", @@ -8832,7 +9078,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": { @@ -8848,7 +9094,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", @@ -8867,13 +9113,13 @@ ] }, "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" }, "proj_axis": { - "title": "Projection plane axis", + "title": "Projection Plane Axis", "description": "Axis along which the observation plane is oriented.", "enum": [ 0, @@ -8883,7 +9129,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", @@ -8932,7 +9178,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": { @@ -8996,8 +9242,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, @@ -9028,7 +9274,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": [ @@ -9068,7 +9314,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": [ From 86d9a768e4267fe1268986c0f14706e3c25a71bd Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 27 Nov 2023 19:22:45 -0500 Subject: [PATCH 57/83] JaxStructureStaticGeometry and JaxStructureStaticMedium to mix differentiable and static geometry / medium in JaxStructure --- CHANGELOG.md | 1 + tests/sims/simulation_2_5_0rc3.h5 | Bin 460416 -> 460416 bytes tests/test_plugins/test_adjoint.py | 18 +- tidy3d/plugins/adjoint/__init__.py | 14 +- tidy3d/plugins/adjoint/components/__init__.py | 6 +- tidy3d/plugins/adjoint/components/medium.py | 10 +- .../plugins/adjoint/components/simulation.py | 37 ++- .../plugins/adjoint/components/structure.py | 244 ++++++++++++++---- 8 files changed, 266 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c588d8acf..44cf068a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 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. ### Changed - 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. diff --git a/tests/sims/simulation_2_5_0rc3.h5 b/tests/sims/simulation_2_5_0rc3.h5 index 6742fa208627f81d0cbdaf185745064b189f0f5d..c93f159de226f150d06d45adcf0142e5b4abdfab 100644 GIT binary patch delta 4695 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 D%kyC) diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index 59eaf6b23..ba9e672b3 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -20,7 +20,11 @@ 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.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 @@ -33,7 +37,7 @@ from tidy3d.plugins.adjoint.utils.penalty import RadiusPenalty from tidy3d.plugins.adjoint.utils.filter import ConicFilter, BinaryProjector, CircularFilter 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 @@ -253,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 @@ -305,6 +317,8 @@ 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], 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/medium.py b/tidy3d/plugins/adjoint/components/medium.py index 3512270bd..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]: diff --git a/tidy3d/plugins/adjoint/components/simulation.py b/tidy3d/plugins/adjoint/components/simulation.py index 08213bc40..62a27dc31 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 @@ -21,7 +21,12 @@ from ....exceptions import AdjointError from .base import JaxObject -from .structure import JaxStructure +from .structure import ( + JaxStructure, + JaxStructureType, + JaxStructureStaticMedium, + JaxStructureStaticGeometry, +) from .geometry import JaxPolySlab, JaxGeometryGroup @@ -82,12 +87,20 @@ class JaxInfo(Tidy3dBaseModel): 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" @@ -171,7 +184,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 @@ -349,6 +363,7 @@ def to_simulation(self) -> Tuple[Simulation, JaxInfo]: 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 @@ -556,7 +571,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) diff --git a/tidy3d/plugins/adjoint/components/structure.py b/tidy3d/plugins/adjoint/components/structure.py index 065cfcb09..c358de6cc 100644 --- a/tidy3d/plugins/adjoint/components/structure.py +++ b/tidy3d/plugins/adjoint/components/structure.py @@ -1,7 +1,7 @@ """Defines a jax-compatible structure and its conversion to a gradient monitor.""" from __future__ import annotations -from typing import List +from typing import List, Union, Dict import pydantic.v1 as pd import numpy as np @@ -12,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, @@ -73,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 minimum wavelength in material (to use for determining integration points) - freq_max = max(grad_data_eps.eps_xx.f) - wvl_free_space = C_0 / freq_max - eps_in = self.medium.eps_model(frequency=freq_max) - 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, freqs: List[float], name: str) -> FieldMonitor: - """Return gradient monitor associated with this object.""" - return self.geometry.make_grad_monitors(freqs=freqs, 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] From d77761675f74fd17486048b988826de0f58cf51e Mon Sep 17 00:00:00 2001 From: Tyler Hughes Date: Mon, 4 Dec 2023 11:18:03 -0500 Subject: [PATCH 58/83] log a warning if any nonlinear medium detected in adjoint sim --- CHANGELOG.md | 9 +++- tests/test_plugins/test_adjoint.py | 52 +++++++++++++++++++ .../plugins/adjoint/components/simulation.py | 41 +++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 44cf068a3..da0927357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### 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. + +### Changed + +### Fixed + ## [2.5.0rc3] - 2023-11-30 ### Added @@ -18,7 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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 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. ### Changed - 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. diff --git a/tests/test_plugins/test_adjoint.py b/tests/test_plugins/test_adjoint.py index ba9e672b3..addad8087 100644 --- a/tests/test_plugins/test_adjoint.py +++ b/tests/test_plugins/test_adjoint.py @@ -1648,3 +1648,55 @@ def J(x): 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/tidy3d/plugins/adjoint/components/simulation.py b/tidy3d/plugins/adjoint/components/simulation.py index 62a27dc31..2b8f422cf 100644 --- a/tidy3d/plugins/adjoint/components/simulation.py +++ b/tidy3d/plugins/adjoint/components/simulation.py @@ -45,6 +45,14 @@ # 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.""" @@ -238,6 +246,39 @@ 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_freqs_adjoint(output_monitors: List[Monitor]) -> List[float]: """Return sorted list of unique frequencies stripped from a collection of monitors.""" From 4c243e6b6daa5e5123dcdeb59ab707569270999e Mon Sep 17 00:00:00 2001 From: momchil Date: Thu, 16 Nov 2023 10:58:42 -0800 Subject: [PATCH 59/83] Some private helper properties in ModeSolver --- CHANGELOG.md | 2 ++ tidy3d/plugins/mode/mode_solver.py | 41 +++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da0927357..59b2be0be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. +- 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 elapses 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. ### Fixed - Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 7f6508741..89b801791 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -16,6 +16,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 @@ -25,7 +26,7 @@ from ...components.data.monitor_data import ModeSolverData from ...exceptions import ValidationError, SetupError from ...constants import C_0 -from .solver import compute_modes +from .solver import compute_modes, EigSolver FIELD = Tuple[ArrayComplex3D, ArrayComplex3D, ArrayComplex3D] MODE_MONITOR_NAME = "<<>>" @@ -593,6 +594,44 @@ 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 at the + mode solver freqs. 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.""" + for int_mat in self._intersecting_media: + if EigSolver.isinstance_complex(int_mat.eps_model(np.array(self.freqs))): + return True + return False + def to_source( self, source_time: SourceTime, From fe7e4e7ddf0a51693d27763ec12e8060e0ca4631 Mon Sep 17 00:00:00 2001 From: momchil Date: Wed, 29 Nov 2023 14:19:57 -0800 Subject: [PATCH 60/83] Avoid error in importing ModeSolver if scipy is not installed --- CHANGELOG.md | 4 ++-- tidy3d/plugins/mode/mode_solver.py | 24 ++++++++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 59b2be0be..d7dc46256 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### 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. ### Fixed @@ -34,8 +36,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. -- 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 elapses 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. ### Fixed - Fixed the duplication of log messages in Jupyter when `set_logging_file` is used. diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 89b801791..2a3764aa1 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -26,7 +26,18 @@ from ...components.data.monitor_data import ModeSolverData from ...exceptions import ValidationError, SetupError from ...constants import C_0 -from .solver import compute_modes, EigSolver + +# 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 = "<<>>" @@ -453,6 +464,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, @@ -628,9 +643,10 @@ def _has_complex_eps(self) -> bool: 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.""" for int_mat in self._intersecting_media: - if EigSolver.isinstance_complex(int_mat.eps_model(np.array(self.freqs))): - return True - return False + max_imag_eps = np.amax(np.abs(np.imag(int_mat.eps_model(np.array(self.freqs))))) + if not np.isclose(max_imag_eps, 0): + return False + return True def to_source( self, From 2d869d4a0b373684bf0e51fc6709d3f63d8a820d Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Mon, 4 Dec 2023 16:47:09 -0600 Subject: [PATCH 61/83] unstructured grid datasets --- CHANGELOG.md | 1 + MANIFEST.in | 1 + requirements/dev.txt | 1 + requirements/vtk.txt | 4 + setup.py | 2 + test_local.sh | 3 + tests/sims/simulation_2_5_0rc3.json | 20 +- tests/test_components/test_heat.py | 96 +- tests/test_data/_test_datasets_no_vtk.py | 37 + tests/test_data/test_data_arrays.py | 51 + tests/test_data/test_datasets.py | 451 +++++++ tidy3d/__init__.py | 7 + tidy3d/components/base_sim/data/sim_data.py | 19 +- tidy3d/components/data/data_array.py | 99 +- tidy3d/components/data/dataset.py | 1212 ++++++++++++++++++- tidy3d/components/heat/data/monitor_data.py | 76 +- tidy3d/components/heat/data/sim_data.py | 183 +-- tidy3d/components/heat/monitor.py | 18 + tidy3d/components/types.py | 17 + tidy3d/components/viz.py | 1 + tidy3d/web/api/container.py | 2 + tox.ini | 2 + 22 files changed, 2155 insertions(+), 148 deletions(-) create mode 100644 requirements/vtk.txt create mode 100644 tests/test_data/_test_datasets_no_vtk.py create mode 100644 tests/test_data/test_datasets.py diff --git a/CHANGELOG.md b/CHANGELOG.md index d7dc46256..3548f023b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - 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. 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/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..4f7e3c87b --- /dev/null +++ b/requirements/vtk.txt @@ -0,0 +1,4 @@ +# vtk + +vtk + diff --git a/setup.py b/setup.py index 82d5efc50..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") @@ -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_2_5_0rc3.json b/tests/sims/simulation_2_5_0rc3.json index f66fe9fcf..4cba7f1c6 100644 --- a/tests/sims/simulation_2_5_0rc3.json +++ b/tests/sims/simulation_2_5_0rc3.json @@ -1011,14 +1011,14 @@ }, "transform": [ [ - 0.9659258262890684, - -0.2588190451025208, + 0.9659258262890683, + -0.25881904510252074, 0.0, 0.0 ], [ - 0.2588190451025208, - 0.9659258262890684, + 0.25881904510252074, + 0.9659258262890683, 0.0, 0.0 ], @@ -1506,7 +1506,7 @@ 1, 1 ], - "colocate": 1, + "colocate": true, "freqs": [ 200000000000000.0, 250000000000000.0 @@ -1538,7 +1538,7 @@ 1, 1 ], - "colocate": 1, + "colocate": true, "start": 0.0, "stop": null, "interval": 1, @@ -1687,7 +1687,7 @@ 1, 1 ], - "colocate": 1, + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1832,7 +1832,7 @@ 1, 1 ], - "colocate": 1, + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1884,7 +1884,7 @@ 1, 1 ], - "colocate": 1, + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 @@ -1933,7 +1933,7 @@ 1, 1 ], - "colocate": 1, + "colocate": true, "freqs": [ 250000000000000.0, 300000000000000.0 diff --git a/tests/test_components/test_heat.py b/tests/test_components/test_heat.py index 988052494..c864703a6 100644 --- a/tests/test_components/test_heat.py +++ b/tests/test_components/test_heat.py @@ -25,6 +25,7 @@ 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 @@ -103,12 +104,17 @@ def test_heat_bcs(): _ = ConvectionBC(ambient_temperature=400, transfer_coeff=-0.2) -def make_heat_mnt(): - return TemperatureMonitor(size=(1, 2, 3), name="test") +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_mnt() + temp_mnt, _, _, _ = make_heat_mnts() with pytest.raises(pd.ValidationError): _ = temp_mnt.updated_copy(name=None) @@ -118,21 +124,75 @@ def test_heat_mnt(): def make_heat_mnt_data(): - temp_mnt = make_heat_mnt() + temp_mnt1, temp_mnt2, temp_mnt3, temp_mnt4 = make_heat_mnts() nx, ny, nz = 9, 6, 5 - x = np.linspace(-1, 1, nx) - y = np.linspace(-2, 2, ny) - z = np.linspace(-3, 3, nz) + 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) - return TemperatureData(monitor=temp_mnt, temperature=temperature_field) + 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 + _ = make_heat_mnt_data() def make_uniform_grid_spec(): @@ -192,7 +252,7 @@ def make_heat_sim(): grid_spec = make_uniform_grid_spec() - temp_mnt = make_heat_mnt() + temp_mnts = make_heat_mnts() heat_sim = HeatSimulation( medium=fluid_medium, @@ -202,7 +262,7 @@ def make_heat_sim(): boundary_spec=[pl1, pl2, pl3, pl4, pl5], grid_spec=grid_spec, sources=[heat_source], - monitors=[temp_mnt], + monitors=temp_mnts, ) return heat_sim @@ -253,7 +313,7 @@ def test_heat_sim(): with pytest.raises(pd.ValidationError): _ = heat_sim.updated_copy(center=(0, 0, 0), size=(1, 0, 0)) - temp_mnt = make_heat_mnt() + temp_mnt = heat_sim.monitors[0] with pytest.raises(pd.ValidationError): heat_sim.updated_copy(monitors=[temp_mnt, temp_mnt]) @@ -337,7 +397,7 @@ def make_heat_sim_data(): heat_sim_data = HeatSimulationData( simulation=heat_sim, - data=[temp_data], + data=temp_data, ) return heat_sim_data @@ -346,15 +406,23 @@ def make_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 = make_heat_mnt() + 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]) 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..deb3bf899 --- /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 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 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..1a29c5c00 --- /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 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 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 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 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 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 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 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 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 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 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 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 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/tidy3d/__init__.py b/tidy3d/__init__.py index c33d9d022..23f072bfd 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -78,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 @@ -319,4 +321,9 @@ def set_logging_level(level: str) -> None: "SpaceModulation", "ContinuousWaveTimeModulation", "ModulationSpec", + "PointDataArray", + "CellDataArray", + "IndexedDataArray", + "TriangularGridDataset", + "TetrahedralGridDataset", ] diff --git a/tidy3d/components/base_sim/data/sim_data.py b/tidy3d/components/base_sim/data/sim_data.py index be2172d95..1707ebc7d 100644 --- a/tidy3d/components/base_sim/data/sim_data.py +++ b/tidy3d/components/base_sim/data/sim_data.py @@ -1,6 +1,6 @@ """Abstract base for simulation data structures.""" from __future__ import annotations -from typing import Dict, Tuple +from typing import Dict, Tuple, Union from abc import ABC @@ -10,6 +10,7 @@ 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 @@ -85,12 +86,14 @@ def validate_no_ambiguity(cls, val, values): return val @staticmethod - def _field_component_value(field_component: xr.DataArray, val: FieldVal) -> xr.DataArray: + 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 : xarray.DataArray + 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. @@ -102,22 +105,22 @@ def _field_component_value(field_component: xr.DataArray, val: FieldVal) -> xr.D """ if val == "real": field_value = field_component.real - field_value.name = f"Re{{{field_component.name}}}" + field_value = field_value.rename(f"Re{{{field_component.name}}}") elif val == "imag": field_value = field_component.imag - field_value.name = f"Im{{{field_component.name}}}" + field_value = field_value.rename(f"Im{{{field_component.name}}}") elif val == "abs": field_value = np.abs(field_component) - field_value.name = f"|{field_component.name}|" + field_value = field_value.rename(f"|{field_component.name}|") elif val == "abs^2": field_value = np.abs(field_component) ** 2 - field_value.name = f"|{field_component.name}|²" + field_value = field_value.rename(f"|{field_component.name}|²") elif val == "phase": field_value = np.arctan2(field_component.imag, field_component.real) - field_value.name = f"∠{field_component.name}" + field_value = field_value.rename(f"∠{field_component.name}") return field_value diff --git a/tidy3d/components/data/data_array.py b/tidy3d/components/data/data_array.py index b0282c1c4..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 = { @@ -321,6 +321,57 @@ def does_cover(self, bounds: Bound) -> bool: 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. @@ -551,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, @@ -571,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 801207bed..f32736a80 100644 --- a/tidy3d/components/data/dataset.py +++ b/tidy3d/components/data/dataset.py @@ -3,22 +3,52 @@ from abc import ABC, abstractmethod from typing import Union, Dict, Callable, Any +import functools 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, VtkCellType, ArrayLike, Ax, Coordinate, Literal, vtk, vtk_id_type +from ...exceptions import DataError, ValidationError, Tidy3dImportError, Tidy3dNotImplementedError from ...log import log +if vtk is not None: + from vtk import vtkCellArray, vtkPoints, vtkUnstructuredGrid, vtkPolyData, vtkPlane + from vtk import vtkLineSource, vtkRectilinearGrid + from vtk import vtkCleanPolyData, vtkXMLUnstructuredGridReader, vtkXMLUnstructuredGridWriter + from vtk import vtkBoxClipDataSet, vtkRemoveUnusedPoints, vtkResampleWithDataSet, vtkPlaneCutter + from vtk import VTK_TRIANGLE, VTK_TETRA, vtkExtractCellsAlongPolyLine + from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtkIdTypeArray, numpy_to_vtk + + +def requires_vtk(fn): + """When decorating a method, requires that vtk is available.""" + + @functools.wraps(fn) + def _fn(*args, **kwargs): + if vtk is None: + 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 + class Dataset(Tidy3dBaseModel, ABC): """Abstract base class for objects that store collections of `:class:`.DataArray`s.""" @@ -454,3 +484,1179 @@ 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) -> VtkCellType: + """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) -> vtkCellArray: + """VTK cell array to use in the VTK representation.""" + cells = vtkCellArray() + cells.SetData( + numpy_to_vtkIdTypeArray(self._vtk_offsets), + numpy_to_vtkIdTypeArray(self.cells.data.ravel()), + ) + return cells + + @property + @requires_vtk + def _vtk_points(self) -> vtkPoints: + """VTK point array to use in the VTK representation.""" + pts = vtkPoints() + pts.SetData(numpy_to_vtk(self._points_3d_array)) + return pts + + @property + @requires_vtk + def _vtk_obj(self) -> vtkUnstructuredGrid: + """A VTK representation (vtkUnstructuredGrid) of the grid.""" + + grid = vtkUnstructuredGrid() + + grid.SetPoints(self._vtk_points) + grid.SetCells(self._vtk_cell_type(), self._vtk_cells) + point_data_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) -> vtkPolyData: + """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 = vtkPlane() + plane.SetOrigin(origin[0], origin[1], origin[2]) + plane.SetNormal(normal[0], normal[1], normal[2]) + + # create cutter + cutter = vtkPlaneCutter() + cutter.SetPlane(plane) + cutter.SetInputData(self._vtk_obj) + cutter.InterpolateAttributesOn() + cutter.Update() + + # clean up the slice + cleaner = 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) -> vtkUnstructuredGrid: + """Load a :class:`vtkUnstructuredGrid` from a file.""" + reader = 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 = vtkXMLUnstructuredGridWriter() + writer.SetFileName(fname) + writer.SetInputData(self._vtk_obj) + writer.Write() + + @classmethod + @requires_vtk + def _get_values_from_vtk( + cls, + vtk_obj: Union[vtkPolyData, vtkUnstructuredGrid], + 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_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 = 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 = 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 = vtkRectilinearGrid() + structured_grid.SetDimensions(shape) + structured_grid.SetXCoordinates(numpy_to_vtk(x)) + structured_grid.SetYCoordinates(numpy_to_vtk(y)) + structured_grid.SetZCoordinates(numpy_to_vtk(z)) + + # create and execute VTK interpolator + interpolator = 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_to_numpy(interpolated.GetPointData().GetAbstractArray(array_id)) + + # fill points without interpolated values + if fill_value != 0: + mask = 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.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) -> VtkCellType: + """VTK cell type to use in the VTK representation.""" + return VTK_TRIANGLE + + @classmethod + @requires_vtk + def _from_vtk_obj(cls, vtk_obj: Union[vtkPolyData, vtkUnstructuredGrid]): + """Initialize from a vtkUnstructuredGrid instance.""" + + # get points cells data from vtk object + if isinstance(vtk_obj, vtkPolyData): + cells_vtk = vtk_obj.GetPolys() + elif isinstance(vtk_obj, vtkUnstructuredGrid): + cells_vtk = vtk_obj.GetCells() + + cells_numpy = vtk_to_numpy(cells_vtk.GetConnectivityArray()) + + cell_offsets = 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_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_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) -> VtkCellType: + """VTK cell type to use in the VTK representation.""" + return VTK_TETRA + + @classmethod + @requires_vtk + def _from_vtk_obj(cls, grid: vtkUnstructuredGrid) -> TetrahedralGridDataset: + """Initialize from a vtkUnstructuredGrid instance.""" + + # read point, cells, and values info from a vtk instance + cells_numpy = vtk_to_numpy(grid.GetCells().GetConnectivityArray()) + points_numpy = vtk_to_numpy(grid.GetPoints().GetData()) + values = cls._get_values_from_vtk(grid, len(points_numpy)) + + # verify cell_types + cells_types = 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 = 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 = 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/heat/data/monitor_data.py b/tidy3d/components/heat/data/monitor_data.py index b14b4fb3d..8f3e4e080 100644 --- a/tidy3d/components/heat/data/monitor_data.py +++ b/tidy3d/components/heat/data/monitor_data.py @@ -1,18 +1,20 @@ """Monitor level data, store the DataArrays associated with a single heat monitor.""" from __future__ import annotations -from typing import Union, Tuple +from typing import Union, Tuple, Optional from abc import ABC -import numpy as np 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 ...types import ScalarSymmetry, Coordinate +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`.""" @@ -47,6 +49,7 @@ class TemperatureData(HeatMonitorData): 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]} ... ) @@ -61,56 +64,49 @@ class TemperatureData(HeatMonitorData): ..., title="Monitor", description="Temperature monitor associated with the data." ) - temperature: SpatialDataArray = pd.Field( - ..., title="Temperature", description="Spatial temperature field.", units=KELVIN + 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() - coords = list(self.temperature.coords.values()) - data = np.array(self.temperature.data) + new_temp = self.temperature for dim in range(3): - if self.symmetry[dim] == 1: - - sym_center = self.symmetry_center[dim] - - if sym_center == coords[dim].data[0]: - num_duplicates = 1 - else: - num_duplicates = 0 - - shape = np.array(np.shape(data)) - old_len = shape[dim] - shape[dim] = 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[dim] = slice(old_len - 1, None, -1) - ind_right[dim] = 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[dim]) - new_coords[old_len - num_duplicates :] = coords[dim] - new_coords[old_len - 1 :: -1] = 2 * sym_center - coords[dim] - - coords[dim] = new_coords - data = new_data + # 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: - coords_dict = dict(zip("xyz", coords)) - new_temperature = SpatialDataArray(data, coords=coords_dict) + new_temp = new_temp.reflect(axis=dim, center=self.symmetry_center[dim]) - return self.updated_copy(temperature=new_temperature, symmetry=(0, 0, 0)) + 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 index e1d909161..e0f5a5305 100644 --- a/tidy3d/components/heat/data/sim_data.py +++ b/tidy3d/components/heat/data/sim_data.py @@ -8,6 +8,8 @@ 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 @@ -95,8 +97,8 @@ def plot_field( Name of :class:`.TemperatureMonitorData` to plot. val : Literal['real', 'abs', 'abs^2'] = 'real' Which part of the field to plot. - scale : Literal['lin', 'dB'] - Plot in linear or logarithmic (dB) scale. + 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). @@ -133,88 +135,127 @@ def plot_field( 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_type = "sequential" - elif val == "real": - cmap_type = "divergent" - else: - cmap_type = "sequential" - - # 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} + 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) - # assert the data is valid for plotting - if len(non_scalar_coords) != 2: + if isinstance(field_data, TetrahedralGridDataset): 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." + "Must select a two-dimensional slice of unstructured dataset for plotting" + " on a plane." ) - spatial_coords_in_data = { - coord_name: (coord_name in non_scalar_coords) for coord_name in "xyz" - } + if isinstance(field_data, TriangularGridDataset): - 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()}." + field_data.plot( + ax=ax, + cmap=cmap, + vmin=vmin, + vmax=vmax, + cbar_kwargs={"label": field_name}, + grid=False, ) - # 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]) + # compute parameters for structures overlay plot + axis = field_data.normal_axis + position = field_data.normal_pos - # select the cross section data - interp_kwarg = {"xyz"[axis]: position} + # 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 cmap_type == "divergent": - cmap = "RdBu_r" - elif cmap_type == "sequential": - cmap = "magma" + if isinstance(field_data, SpatialDataArray): - # plot the field - 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": "temperature"}, - ) + # 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, @@ -224,9 +265,7 @@ def plot_field( ) # set the limits based on the xarray coordinates min and max - x_coord_values = field_data.coords[x_coord_label] - y_coord_values = field_data.coords[y_coord_label] - ax.set_xlim(min(x_coord_values), max(x_coord_values)) - ax.set_ylim(min(y_coord_values), max(y_coord_values)) + 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/monitor.py b/tidy3d/components/heat/monitor.py index 46e216707..1fd722a48 100644 --- a/tidy3d/components/heat/monitor.py +++ b/tidy3d/components/heat/monitor.py @@ -1,5 +1,6 @@ """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 @@ -16,6 +17,23 @@ class HeatMonitor(AbstractMonitor, ABC): 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 diff --git a/tidy3d/components/types.py b/tidy3d/components/types.py index b7a7f7422..cb8f2094c 100644 --- a/tidy3d/components/types.py +++ b/tidy3d/components/types.py @@ -15,11 +15,24 @@ from shapely.geometry.base import BaseGeometry from ..exceptions import ValidationError + try: import trimesh except ImportError: trimesh = None +try: + import vtk + from vtkmodules.vtkCommonCore import vtkLogger + + vtkLogger.SetStderrVerbosity(vtkLogger.VERBOSITY_WARNING) + + vtk_id_type = np.int32 if vtk.vtkIdTypeArray().GetDataTypeSize() == 4 else np.int64 + +except ImportError: + vtk = None + vtk_id_type = np.int64 + # type tag default name TYPE_TAG_STR = "type" @@ -246,3 +259,7 @@ def __modify_schema__(cls, field_schema): """ mode tracking """ TrackFreq = Literal["central", "lowest", "highest"] + +""" VTK """ + +VtkCellType = Any if vtk is None else Literal[vtk.VTK_TRIANGLE, vtk.VTK_TETRA] diff --git a/tidy3d/components/viz.py b/tidy3d/components/viz.py index 43f979f73..3f7ea4abd 100644 --- a/tidy3d/components/viz.py +++ b/tidy3d/components/viz.py @@ -115,6 +115,7 @@ def to_kwargs(self) -> dict: 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 = [ diff --git a/tidy3d/web/api/container.py b/tidy3d/web/api/container.py index df1da2340..58229d6ff 100644 --- a/tidy3d/web/api/container.py +++ b/tidy3d/web/api/container.py @@ -402,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(): @@ -409,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) diff --git a/tox.ini b/tox.ini index a81312de8..ac0b2cc14 100644 --- a/tox.ini +++ b/tox.ini @@ -23,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 From d66306ed9ac953363007e166f290c91879757b87 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Mon, 4 Dec 2023 19:20:05 -0600 Subject: [PATCH 62/83] remove the now obsolete heat solver warning --- tidy3d/components/heat/simulation.py | 32 ---------------------------- 1 file changed, 32 deletions(-) diff --git a/tidy3d/components/heat/simulation.py b/tidy3d/components/heat/simulation.py index 3fcb0bdcd..b3608614f 100644 --- a/tidy3d/components/heat/simulation.py +++ b/tidy3d/components/heat/simulation.py @@ -23,7 +23,6 @@ from ..geometry.primitives import Sphere, Cylinder from ..geometry.polyslab import PolySlab from ..scene import Scene -from ..heat_spec import SolidSpec from ..bc_placement import StructureBoundary, StructureStructureInterface from ..bc_placement import StructureSimulationBoundary, SimulationBoundary @@ -206,37 +205,6 @@ def names_exist_sources(cls, val, values): ) return val - """ Post-init validators """ - - def _post_init_validators(self) -> None: - """Call validators taking z`self` that get run after init.""" - self._warn_multiple_zones() - - def _warn_multiple_zones(self): - """Warn about current restriction on number of adjacent zones.""" - - struc_src_map = {} - for source in self.sources: - for name in source.structures: - struc_src_map[name] = source - - unique_solid_zones = { - (struc.medium.heat_spec, struc_src_map.get(struc.name, None)) - for struc in self.structures - if isinstance(struc.medium.heat_spec, SolidSpec) - } - - if isinstance(self.medium.heat_spec, SolidSpec): - unique_solid_zones.add((self.medium.heat_spec, None)) - - if len(unique_solid_zones) > 2: - log.warning( - "More than 2 different solid zones (zone = medium + source) are detected in the heat " - "simulation. Make sure no more than 2 solid zones are adjacent to each other anywhere " - "in the simulation domain. The simulation results may be inaccurate otherwise. " - "This restriction will be removed in the upcoming Tidy3D versions." - ) - @equal_aspect @add_ax_if_none def plot_heat_conductivity( From 676f351a0ebee01030942dd66c84b9b55e775b6b Mon Sep 17 00:00:00 2001 From: Shashwat Sharma Date: Tue, 28 Nov 2023 15:48:44 -0500 Subject: [PATCH 63/83] add windowing feature to field projection monitors - revisions based on PR 1275 review - update docstring and comment --- CHANGELOG.md | 1 + tests/sims/simulation_2_5_0rc3.json | 16 ++++ tests/test_components/test_monitor.py | 20 +++++ tidy3d/components/field_projection.py | 76 ++++++++++++++++-- tidy3d/components/monitor.py | 106 +++++++++++++++++++++++++- 5 files changed, 212 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3548f023b..1a066b0fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### Changed - 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. diff --git a/tests/sims/simulation_2_5_0rc3.json b/tests/sims/simulation_2_5_0rc3.json index 4cba7f1c6..49a6bee9c 100644 --- a/tests/sims/simulation_2_5_0rc3.json +++ b/tests/sims/simulation_2_5_0rc3.json @@ -1706,6 +1706,10 @@ 3.0 ], "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], "proj_distance": 1000000.0, "theta": [ -1.5707963267948966, @@ -1851,6 +1855,10 @@ 3.0 ], "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], "proj_axis": 2, "proj_distance": 5.0, "x": [ @@ -1903,6 +1911,10 @@ 3.0 ], "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], "proj_axis": 2, "proj_distance": 1000000.0, "ux": [ @@ -1952,6 +1964,10 @@ 3.0 ], "far_field_approx": true, + "window_size": [ + 0.0, + 0.0 + ], "proj_distance": 1000000.0, "theta": [ -1.5707963267948966, diff --git a/tests/test_components/test_monitor.py b/tests/test_components/test_monitor.py index 2afc3d432..8e9553595 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( diff --git a/tidy3d/components/field_projection.py b/tidy3d/components/field_projection.py index 175bf1468..6de57add0 100644 --- a/tidy3d/components/field_projection.py +++ b/tidy3d/components/field_projection.py @@ -459,6 +459,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: @@ -514,10 +553,17 @@ def _project_fields_angular( 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, ) for field, _field in zip(fields, _fields): field[..., idx_f] += _field * phase[idx_f] @@ -534,7 +580,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 ) for field, _field in zip(fields, _fields): field[0, i, j, :] += _field @@ -596,16 +642,25 @@ 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, ) 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 ) for field, _field in zip(fields, _fields): field[i, j, k, :] += _field @@ -658,10 +713,19 @@ 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, ) for field, _field in zip(fields, _fields): field[i, j, 0, idx_f] += _field * phase[idx_f] @@ -669,7 +733,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 ) for field, _field in zip(fields, _fields): field[i, j, 0, :] += _field diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 074c65e03..ac8d8f1ad 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -5,7 +5,7 @@ import pydantic.v1 as pydantic import numpy as np -from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound +from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound, Bound, Size from .types import Literal, Direction, Coordinate, Axis, ObsGridArray, BoxSurface from .validators import assert_plane from .base import cached_property, Tidy3dBaseModel @@ -24,6 +24,12 @@ 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(AbstractMonitor): """Abstract base class for monitors.""" @@ -666,6 +672,48 @@ class AbstractFieldProjectionMonitor(SurfaceIntegrationMonitor, FreqMonitor): "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).", + ) + + @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.""" @@ -691,6 +739,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 From 8d620e6fa9a1e7f7298a51fcec61a40d47a384a3 Mon Sep 17 00:00:00 2001 From: momchil Date: Tue, 5 Dec 2023 12:48:21 -0800 Subject: [PATCH 64/83] Fix to fully anisotropic eps_model when an array of freqs is provided --- tests/test_components/test_medium.py | 3 +++ tidy3d/components/medium.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/test_components/test_medium.py b/tests/test_components/test_medium.py index 29d8f1966..d78e90af2 100644 --- a/tests/test_components/test_medium.py +++ b/tests/test_components/test_medium.py @@ -450,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) diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index 2779c4093..bd34a0b35 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -3606,6 +3606,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) From 914790fc02ccee055fe53b4b1f43697abd27488c Mon Sep 17 00:00:00 2001 From: momchil Date: Sat, 2 Dec 2023 14:06:22 -0800 Subject: [PATCH 65/83] Error if any frequencies are smaller than 1e5 Hz --- CHANGELOG.md | 1 + tests/sims/simulation_1_10_0.json | 1382 ----------------- tests/sims/simulation_1_10_0rc1.json | 1354 ---------------- tests/sims/simulation_1_10_0rc2.json | 1382 ----------------- tests/sims/simulation_1_3_3.json | 337 ---- tests/sims/simulation_1_4_0.json | 575 ------- tests/sims/simulation_1_4_1.json | 575 ------- tests/sims/simulation_1_5_0.json | 593 ------- tests/sims/simulation_1_6_0.json | 564 ------- tests/sims/simulation_1_6_1.json | 564 ------- tests/sims/simulation_1_6_2.json | 565 ------- tests/sims/simulation_1_6_3.json | 565 ------- tests/sims/simulation_1_7_0.json | 1101 ------------- tests/sims/simulation_1_7_1.json | 1108 ------------- tests/sims/simulation_1_8_0.json | 1 - tests/sims/simulation_1_8_1.json | 1 - tests/sims/simulation_1_8_2.json | 1 - tests/sims/simulation_1_8_3.json | 1 - tests/sims/simulation_1_8_4.json | 1 - tests/sims/simulation_1_9_0.json | 1364 ---------------- tests/sims/simulation_1_9_0rc1.json | 1 - tests/sims/simulation_1_9_0rc2.json | 1336 ---------------- tests/sims/simulation_1_9_1.json | 1336 ---------------- tests/sims/simulation_1_9_2.json | 1336 ---------------- tests/test_components/test_base.py | 4 +- .../test_components/test_field_projection.py | 2 +- tests/test_components/test_geometry.py | 2 +- tests/test_components/test_monitor.py | 39 +- tests/test_components/test_simulation.py | 72 +- tests/test_components/test_source.py | 59 +- tests/test_package/test_log.py | 2 +- tests/test_plugins/test_mode_solver.py | 6 +- tests/utils.py | 7 +- tidy3d/components/monitor.py | 10 +- tidy3d/components/source.py | 8 +- tidy3d/components/validators.py | 37 + tidy3d/plugins/mode/mode_solver.py | 23 +- 37 files changed, 157 insertions(+), 16158 deletions(-) delete mode 100644 tests/sims/simulation_1_10_0.json delete mode 100644 tests/sims/simulation_1_10_0rc1.json delete mode 100644 tests/sims/simulation_1_10_0rc2.json delete mode 100644 tests/sims/simulation_1_3_3.json delete mode 100644 tests/sims/simulation_1_4_0.json delete mode 100644 tests/sims/simulation_1_4_1.json delete mode 100644 tests/sims/simulation_1_5_0.json delete mode 100644 tests/sims/simulation_1_6_0.json delete mode 100644 tests/sims/simulation_1_6_1.json delete mode 100644 tests/sims/simulation_1_6_2.json delete mode 100644 tests/sims/simulation_1_6_3.json delete mode 100644 tests/sims/simulation_1_7_0.json delete mode 100644 tests/sims/simulation_1_7_1.json delete mode 100644 tests/sims/simulation_1_8_0.json delete mode 100644 tests/sims/simulation_1_8_1.json delete mode 100644 tests/sims/simulation_1_8_2.json delete mode 100644 tests/sims/simulation_1_8_3.json delete mode 100644 tests/sims/simulation_1_8_4.json delete mode 100644 tests/sims/simulation_1_9_0.json delete mode 100644 tests/sims/simulation_1_9_0rc1.json delete mode 100644 tests/sims/simulation_1_9_0rc2.json delete mode 100644 tests/sims/simulation_1_9_1.json delete mode 100644 tests/sims/simulation_1_9_2.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a066b0fc..9ff020ed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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`). ### Fixed diff --git a/tests/sims/simulation_1_10_0.json b/tests/sims/simulation_1_10_0.json deleted file mode 100644 index a977b094a..000000000 --- a/tests/sims/simulation_1_10_0.json +++ /dev/null @@ -1,1382 +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 - } - }, - { - "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.10.0" -} \ No newline at end of file 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_10_0rc2.json b/tests/sims/simulation_1_10_0rc2.json deleted file mode 100644 index 132acc9d9..000000000 --- a/tests/sims/simulation_1_10_0rc2.json +++ /dev/null @@ -1,1382 +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": "middle", - "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": "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, - "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 - } - }, - { - "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.10.0rc2" -} \ 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/test_components/test_base.py b/tests/test_components/test_base.py index b1424e834..08099b4a9 100644 --- a/tests/test_components/test_base.py +++ b/tests/test_components/test_base.py @@ -120,7 +120,7 @@ def test_updated_copy(): def test_equality(): # test freqs / arraylike - mnt1 = td.FluxMonitor(size=(1, 1, 0), freqs=np.array([1, 2, 3]), name="1") - mnt2 = td.FluxMonitor(size=(1, 1, 0), freqs=np.array([1, 2, 3]), name="1") + 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_field_projection.py b/tests/test_components/test_field_projection.py index 36436e955..8e1208292 100644 --- a/tests/test_components/test_field_projection.py +++ b/tests/test_components/test_field_projection.py @@ -274,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) diff --git a/tests/test_components/test_geometry.py b/tests/test_components/test_geometry.py index edf827fb7..ad0568213 100644 --- a/tests/test_components/test_geometry.py +++ b/tests/test_components/test_geometry.py @@ -291,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="+") diff --git a/tests/test_components/test_monitor.py b/tests/test_components/test_monitor.py index 8e9553595..22f68a412 100644 --- a/tests/test_components/test_monitor.py +++ b/tests/test_components/test_monitor.py @@ -250,7 +250,7 @@ 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), ) @@ -259,7 +259,7 @@ 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), colocate=False, @@ -267,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, ) @@ -316,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) @@ -353,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(): @@ -377,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_simulation.py b/tests/test_components/test_simulation.py index c66eaa7c6..17a944301 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -62,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), @@ -146,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), @@ -282,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", ) @@ -306,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], @@ -338,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", ) @@ -530,14 +530,16 @@ 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() @@ -738,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") @@ -1233,9 +1235,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()), ) @@ -1296,8 +1298,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()), ) @@ -1305,7 +1307,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): @@ -1328,7 +1330,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()), ) @@ -1353,7 +1355,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()), @@ -1362,7 +1364,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], @@ -1386,7 +1388,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], @@ -1469,7 +1471,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], @@ -1595,7 +1597,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, @@ -1616,12 +1618,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() ) ], ) @@ -1641,7 +1643,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), ) ], ) @@ -1660,12 +1662,14 @@ def test_error_large_monitors(): ) mnt_size = (td.inf, 0, td.inf) mnt_test = [ - td.ModeMonitor(size=mnt_size, freqs=[1], name="test", mode_spec=td.ModeSpec()), - td.ModeSolverMonitor(size=mnt_size, freqs=[1], name="test", mode_spec=td.ModeSpec()), - td.FluxMonitor(size=mnt_size, freqs=[1], name="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=[1], name="test"), - td.FieldProjectionAngleMonitor(size=mnt_size, freqs=[1], name="test", theta=[0], phi=[0]), + 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: @@ -2053,7 +2057,7 @@ def test_to_gds(tmp_path): ) ], 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"), ], boundary_spec=td.BoundarySpec( x=td.Boundary.pml(num_layers=20), diff --git a/tests/test_components/test_source.py b/tests/test_components/test_source.py index a756df266..ac4a7a95e 100644 --- a/tests/test_components/test_source.py +++ b/tests/test_components/test_source.py @@ -47,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) @@ -57,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() @@ -66,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) @@ -93,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 @@ -153,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): @@ -192,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 @@ -270,22 +268,32 @@ def check_freq_grid(freq_grid, num_freqs): def test_custom_source_time(log_capture): - ts = np.linspace(0, 30, 1001) + 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) + 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 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=[1], name="field") + monitor = td.FieldMonitor(size=(td.inf, td.inf, 0), freqs=[1e12], name="field") sim = td.Simulation( size=(10, 10, 10), run_time=1e-12, @@ -293,12 +301,15 @@ def test_custom_source_time(log_capture): sources=[source], normalize_index=None, ) - 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) @@ -307,7 +318,7 @@ def test_custom_source_time(log_capture): 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=1, fwidth=0.1)) + 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") @@ -315,5 +326,5 @@ def test_custom_source_time(log_capture): 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) + 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_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_mode_solver.py b/tests/test_plugins/test_mode_solver.py index 45c3d2b2d..6a77d3a72 100644 --- a/tests/test_plugins/test_mode_solver.py +++ b/tests/test_plugins/test_mode_solver.py @@ -489,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(): @@ -711,7 +711,7 @@ def test_mode_solver_method_defaults(): ) # test defaults - st = td.GaussianPulse(freq0=1.0, fwidth=1.0) + st = td.GaussianPulse(freq0=1.0e12, fwidth=1.0e12) src = ms.to_source(source_time=st) assert src.direction == ms.direction diff --git a/tests/utils.py b/tests/utils.py index 6c82bf43e..28a11799d 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", ), diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index ac8d8f1ad..8b709b9c2 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -7,7 +7,7 @@ from .types import Ax, EMField, ArrayFloat1D, FreqArray, FreqBound, Bound, Size from .types import Literal, Direction, Coordinate, Axis, ObsGridArray, BoxSurface -from .validators import assert_plane +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 @@ -80,12 +80,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): diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index b2cc9f6e2..b2d150bce 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -15,7 +15,7 @@ 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, get_value -from .validators import warn_if_dataset_none, assert_single_freq_in_range +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 @@ -372,6 +372,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/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/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 2a3764aa1..0a875ff38 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -24,6 +24,8 @@ from ...components.data.data_array import FreqModeDataArray from ...components.data.sim_data import SimulationData from ...components.data.monitor_data import ModeSolverData + +from ...components.validators import validate_freqs_min, validate_freqs_not_empty from ...exceptions import ValidationError, SetupError from ...constants import C_0 @@ -42,9 +44,6 @@ 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 @@ -96,22 +95,8 @@ 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 - - @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'." - ) - return val + _freqs_not_empty = validate_freqs_not_empty() + _freqs_lower_bound = validate_freqs_min() @cached_property def normal_axis(self) -> Axis: From 2156b4288965b2d8e2006afd7a7265471af95a72 Mon Sep 17 00:00:00 2001 From: Shashwat Sharma Date: Mon, 4 Dec 2023 22:33:45 -0500 Subject: [PATCH 66/83] add field projection validators based on user experiences --- CHANGELOG.md | 2 + tests/sims/simulation_2_5_0rc3.h5 | Bin 460416 -> 459872 bytes tests/sims/simulation_2_5_0rc3.json | 414 +++++++++++------------ tests/test_components/test_simulation.py | 161 +++++++++ tests/utils.py | 12 +- tidy3d/components/simulation.py | 92 +++++ 6 files changed, 468 insertions(+), 213 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ff020ed9..6f0e928f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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. ### 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. diff --git a/tests/sims/simulation_2_5_0rc3.h5 b/tests/sims/simulation_2_5_0rc3.h5 index c93f159de226f150d06d45adcf0142e5b4abdfab..fed60dfa99be8f425a10e019a319fbd20c8cb3a7 100644 GIT binary patch delta 15647 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

hj3N856T>|`$ORd+;wWLO#?D|O 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 GS@{1OB9<8d delta 15484 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 XivL1@?pOS;dT>SQK(T_iEsFE+%o2$% diff --git a/tests/sims/simulation_2_5_0rc3.json b/tests/sims/simulation_2_5_0rc3.json index 49a6bee9c..a2d5ab982 100644 --- a/tests/sims/simulation_2_5_0rc3.json +++ b/tests/sims/simulation_2_5_0rc3.json @@ -1712,110 +1712,110 @@ ], "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 + 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, - 1.5707963267948966 + 0.5235987755982988 ] }, { @@ -1918,13 +1918,13 @@ "proj_axis": 2, "proj_distance": 1000000.0, "ux": [ - 0.1, - 0.2 + 0.02, + 0.04 ], "uy": [ - 0.3, - 0.4, - 0.5 + 0.03, + 0.04, + 0.05 ] }, { @@ -1970,110 +1970,110 @@ ], "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 + 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, - 1.5707963267948966 + 0.39269908169872414 ] }, { diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 17a944301..a735635d3 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -1035,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.""" diff --git a/tests/utils.py b/tests/utils.py index 28a11799d..95a7be45c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -437,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), @@ -458,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), @@ -467,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/components/simulation.py b/tidy3d/components/simulation.py index 6204df2fa..e1877f4a6 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -31,6 +31,7 @@ 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 @@ -638,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.""" From 5b2f7aca578d759d9cf88f962519eab6ec1a2557 Mon Sep 17 00:00:00 2001 From: momchil Date: Wed, 6 Dec 2023 12:07:08 -0800 Subject: [PATCH 67/83] Making bend radius a positive float --- tidy3d/components/mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/components/mode.py b/tidy3d/components/mode.py index 376e919dd..0620a30e9 100644 --- a/tidy3d/components/mode.py +++ b/tidy3d/components/mode.py @@ -77,7 +77,7 @@ class ModeSpec(Tidy3dBaseModel): "single precision, but more accurate under double precision.", ) - bend_radius: float = pd.Field( + bend_radius: pd.PositiveFloat = pd.Field( None, title="Bend radius", description="A curvature radius for simulation of waveguide bends. Can be negative, in " From 99e0aafde05b02a3c939001330e3d7a92039b681 Mon Sep 17 00:00:00 2001 From: momchil Date: Wed, 6 Dec 2023 11:25:53 -0800 Subject: [PATCH 68/83] Replacing np.isclose with math.isclose in transformed validator --- tidy3d/components/geometry/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tidy3d/components/geometry/utils.py b/tidy3d/components/geometry/utils.py index 763ff7ec9..fa4d764da 100644 --- a/tidy3d/components/geometry/utils.py +++ b/tidy3d/components/geometry/utils.py @@ -1,6 +1,7 @@ """Utilities for geometry manipulation.""" from __future__ import annotations from typing import Union, Tuple +from math import isclose import numpy as np @@ -192,7 +193,7 @@ def validate_no_transformed_polyslabs(geometry: GeometryType, transform: MatrixR transform = np.eye(4) if isinstance(geometry, polyslab.PolySlab): if not ( - np.isclose(geometry.sidewall_angle, 0) + isclose(geometry.sidewall_angle, 0) or base.Transformed.preserves_axis(transform, geometry.axis) ): raise Tidy3dError( From 7fc2c2db808cde942904e57dc16981a9ffab9547 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Wed, 6 Dec 2023 15:45:57 -0600 Subject: [PATCH 69/83] on-the-fly vtk import --- tests/test_data/_test_datasets_no_vtk.py | 4 +- tests/test_data/test_datasets.py | 24 ++-- tidy3d/components/data/dataset.py | 137 +++++++++-------------- tidy3d/components/types.py | 53 ++++++--- 4 files changed, 110 insertions(+), 108 deletions(-) diff --git a/tests/test_data/_test_datasets_no_vtk.py b/tests/test_data/_test_datasets_no_vtk.py index deb3bf899..f777be98f 100644 --- a/tests/test_data/_test_datasets_no_vtk.py +++ b/tests/test_data/_test_datasets_no_vtk.py @@ -24,7 +24,7 @@ def test_triangular_dataset_no_vtk(tmp_path): # double check that vtk was not imported from tidy3d.components.types import vtk - assert vtk is None + assert vtk["mod"] is None @pytest.mark.usefixtures("hide_vtk") @@ -34,4 +34,4 @@ def test_tetrahedral_dataset_no_vtk(tmp_path): # double check that vtk was not imported from tidy3d.components.types import vtk - assert vtk is None + assert vtk["mod"] is None diff --git a/tests/test_data/test_datasets.py b/tests/test_data/test_datasets.py index 1a29c5c00..65ad2b4e3 100644 --- a/tests/test_data/test_datasets.py +++ b/tests/test_data/test_datasets.py @@ -124,7 +124,7 @@ def test_triangular_dataset(tmp_path, ds_name): 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 is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): _ = tri_grid._vtk_cells with pytest.raises(Tidy3dImportError): @@ -137,7 +137,7 @@ def test_triangular_dataset(tmp_path, ds_name): _ = tri_grid._vtk_obj # plane slicing - if vtk is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): _ = tri_grid.plane_slice(axis=2, pos=0.5) else: @@ -154,7 +154,7 @@ def test_triangular_dataset(tmp_path, ds_name): _ = tri_grid.plane_slice(axis=0, pos=2) # clipping by a box - if vtk is None: + 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: @@ -166,7 +166,7 @@ def test_triangular_dataset(tmp_path, ds_name): _ = tri_grid.box_clip([[0.1, 0.1, 0.3], [0.2, 0.2, 0.9]]) # interpolation - if vtk is None: + 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 @@ -224,7 +224,7 @@ def test_triangular_dataset(tmp_path, ds_name): _ = tri_grid.plot(field=False, grid=False) # generalized selection method - if vtk is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): _ = tri_grid.sel(x=0.2) else: @@ -238,7 +238,7 @@ def test_triangular_dataset(tmp_path, ds_name): _ = tri_grid.sel(x=np.linspace(0, 1, 3), y=1.2, z=[0.3, 0.4, 0.5]) # writting/reading .vtu - if vtk is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): tri_grid.to_vtu(tmp_path / "tri_grid_test.vtu") with pytest.raises(Tidy3dImportError): @@ -362,7 +362,7 @@ def test_tetrahedral_dataset(tmp_path, ds_name): assert np.all(tet_grid._vtk_offsets == np.array([0, 4, 8])) assert tet_grid.name == ds_name - if vtk is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): _ = tet_grid._vtk_cells with pytest.raises(Tidy3dImportError): @@ -375,7 +375,7 @@ def test_tetrahedral_dataset(tmp_path, ds_name): _ = tet_grid._vtk_obj # plane slicing - if vtk is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): _ = tet_grid.plane_slice(axis=2, pos=0.5) else: @@ -387,7 +387,7 @@ def test_tetrahedral_dataset(tmp_path, ds_name): _ = tet_grid.plane_slice(axis=1, pos=2) # clipping by a box - if vtk is None: + 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: @@ -399,7 +399,7 @@ def test_tetrahedral_dataset(tmp_path, ds_name): _ = tet_grid.box_clip([[0.1, 1.1, 0.3], [0.2, 1.2, 0.9]]) # interpolation - if vtk is None: + 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: @@ -415,7 +415,7 @@ def test_tetrahedral_dataset(tmp_path, ds_name): assert no_intersection.name == ds_name # generalized selection method - if vtk is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): _ = tet_grid.sel(x=0.2) else: @@ -429,7 +429,7 @@ def test_tetrahedral_dataset(tmp_path, ds_name): _ = tet_grid.sel(x=0.2, z=[0.3, 0.4, 0.5]) # writting/reading .vtu - if vtk is None: + if vtk["mod"] is None: with pytest.raises(Tidy3dImportError): tet_grid.to_vtu(tmp_path / "tet_grid_test.vtu") with pytest.raises(Tidy3dImportError): diff --git a/tidy3d/components/data/dataset.py b/tidy3d/components/data/dataset.py index f32736a80..1cd13767a 100644 --- a/tidy3d/components/data/dataset.py +++ b/tidy3d/components/data/dataset.py @@ -3,7 +3,6 @@ from abc import ABC, abstractmethod from typing import Union, Dict, Callable, Any -import functools import xarray as xr import numpy as np @@ -21,34 +20,11 @@ from ..viz import equal_aspect, add_ax_if_none, plot_params_grid from ..base import Tidy3dBaseModel, cached_property -from ..types import Axis, Bound, VtkCellType, ArrayLike, Ax, Coordinate, Literal, vtk, vtk_id_type -from ...exceptions import DataError, ValidationError, Tidy3dImportError, Tidy3dNotImplementedError +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 -if vtk is not None: - from vtk import vtkCellArray, vtkPoints, vtkUnstructuredGrid, vtkPolyData, vtkPlane - from vtk import vtkLineSource, vtkRectilinearGrid - from vtk import vtkCleanPolyData, vtkXMLUnstructuredGridReader, vtkXMLUnstructuredGridWriter - from vtk import vtkBoxClipDataSet, vtkRemoveUnusedPoints, vtkResampleWithDataSet, vtkPlaneCutter - from vtk import VTK_TRIANGLE, VTK_TETRA, vtkExtractCellsAlongPolyLine - from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtkIdTypeArray, numpy_to_vtk - - -def requires_vtk(fn): - """When decorating a method, requires that vtk is available.""" - - @functools.wraps(fn) - def _fn(*args, **kwargs): - if vtk is None: - 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 - class Dataset(Tidy3dBaseModel, ABC): """Abstract base class for objects that store collections of `:class:`.DataArray`s.""" @@ -521,7 +497,7 @@ def match_cells_to_vtk_type(cls, val): 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) + 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): @@ -663,7 +639,7 @@ def _cell_num_vertices(cls) -> pd.PositiveInt: @classmethod @abstractmethod @requires_vtk - def _vtk_cell_type(cls) -> VtkCellType: + def _vtk_cell_type(cls): """VTK cell type to use in the VTK representation.""" @cached_property @@ -673,44 +649,44 @@ def _vtk_offsets(self) -> ArrayLike: if vtk is None: return offsets - return offsets.astype(vtk_id_type, copy=False) + return offsets.astype(vtk["id_type"], copy=False) @property @requires_vtk - def _vtk_cells(self) -> vtkCellArray: + def _vtk_cells(self): """VTK cell array to use in the VTK representation.""" - cells = vtkCellArray() + cells = vtk["mod"].vtkCellArray() cells.SetData( - numpy_to_vtkIdTypeArray(self._vtk_offsets), - numpy_to_vtkIdTypeArray(self.cells.data.ravel()), + vtk["numpy_to_vtkIdTypeArray"](self._vtk_offsets), + vtk["numpy_to_vtkIdTypeArray"](self.cells.data.ravel()), ) return cells @property @requires_vtk - def _vtk_points(self) -> vtkPoints: + def _vtk_points(self): """VTK point array to use in the VTK representation.""" - pts = vtkPoints() - pts.SetData(numpy_to_vtk(self._points_3d_array)) + pts = vtk["mod"].vtkPoints() + pts.SetData(vtk["numpy_to_vtk"](self._points_3d_array)) return pts @property @requires_vtk - def _vtk_obj(self) -> vtkUnstructuredGrid: + def _vtk_obj(self): """A VTK representation (vtkUnstructuredGrid) of the grid.""" - grid = vtkUnstructuredGrid() + grid = vtk["mod"].vtkUnstructuredGrid() grid.SetPoints(self._vtk_points) grid.SetCells(self._vtk_cell_type(), self._vtk_cells) - point_data_vtk = numpy_to_vtk(self.values.data) + 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) -> vtkPolyData: + 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]: @@ -726,19 +702,19 @@ def _plane_slice_raw(self, axis: Axis, pos: float) -> vtkPolyData: normal[axis] = 1 # create cutting plane - plane = vtkPlane() + plane = vtk["mod"].vtkPlane() plane.SetOrigin(origin[0], origin[1], origin[2]) plane.SetNormal(normal[0], normal[1], normal[2]) # create cutter - cutter = vtkPlaneCutter() + cutter = vtk["mod"].vtkPlaneCutter() cutter.SetPlane(plane) cutter.SetInputData(self._vtk_obj) cutter.InterpolateAttributesOn() cutter.Update() # clean up the slice - cleaner = vtkCleanPolyData() + cleaner = vtk["mod"].vtkCleanPolyData() cleaner.SetInputData(cutter.GetOutput()) cleaner.Update() @@ -767,9 +743,9 @@ def plane_slice( @staticmethod @requires_vtk - def _read_vtkUnstructuredGrid(fname: str) -> vtkUnstructuredGrid: + def _read_vtkUnstructuredGrid(fname: str): """Load a :class:`vtkUnstructuredGrid` from a file.""" - reader = vtkXMLUnstructuredGridReader() + reader = vtk["mod"].vtkXMLUnstructuredGridReader() reader.SetFileName(fname) reader.Update() grid = reader.GetOutput() @@ -810,18 +786,14 @@ def to_vtu(self, fname: str): Full path to the .vtu file to save the unstructured data to. """ - writer = vtkXMLUnstructuredGridWriter() + 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: Union[vtkPolyData, vtkUnstructuredGrid], - num_points: pd.PositiveInt, - ) -> IndexedDataArray: + 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() @@ -860,7 +832,7 @@ def _get_values_from_vtk( f"The length of found point data array ({num_tuples}) does not match the number of grid points ({num_points})." ) - values_numpy = vtk_to_numpy(array_vtk) + values_numpy = vtk["vtk_to_numpy"](array_vtk) values_name = array_vtk.GetName() values = IndexedDataArray( @@ -885,7 +857,7 @@ def box_clip(self, bounds: Bound) -> UnstructuredGridDataset: """ # make and run a VTK clipper - clipper = vtkBoxClipDataSet() + 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] @@ -897,7 +869,7 @@ def box_clip(self, bounds: Bound) -> UnstructuredGridDataset: clip = clipper.GetOutput() # cleann grid from unused points - grid_cleaner = vtkRemoveUnusedPoints() + grid_cleaner = vtk["mod"].vtkRemoveUnusedPoints() grid_cleaner.SetInputData(clip) grid_cleaner.GenerateOriginalPointIdsOff() grid_cleaner.Update() @@ -943,14 +915,14 @@ def interp( shape = (len(x), len(y), len(z)) # create a VTK rectilinear grid to sample onto - structured_grid = vtkRectilinearGrid() + structured_grid = vtk["mod"].vtkRectilinearGrid() structured_grid.SetDimensions(shape) - structured_grid.SetXCoordinates(numpy_to_vtk(x)) - structured_grid.SetYCoordinates(numpy_to_vtk(y)) - structured_grid.SetZCoordinates(numpy_to_vtk(z)) + 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 = vtkResampleWithDataSet() + interpolator = vtk["mod"].vtkResampleWithDataSet() interpolator.SetInputData(structured_grid) interpolator.SetSourceData(self._vtk_obj) interpolator.Update() @@ -958,11 +930,13 @@ def interp( # get results in a numpy representation array_id = 0 if self.values.name is None else self.values.name - values_numpy = vtk_to_numpy(interpolated.GetPointData().GetAbstractArray(array_id)) + values_numpy = vtk["vtk_to_numpy"](interpolated.GetPointData().GetAbstractArray(array_id)) # fill points without interpolated values if fill_value != 0: - mask = vtk_to_numpy(interpolated.GetPointData().GetAbstractArray("vtkValidPointMask")) + 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 @@ -1019,7 +993,7 @@ def reflect( Data after reflextion is performed. """ - reflector = vtk.vtkReflectionFilter() + reflector = vtk["mod"].vtkReflectionFilter() reflector.SetPlane([reflector.USE_X, reflector.USE_Y, reflector.USE_Z][axis]) reflector.SetCenter(center) reflector.SetCopyInput(not reflection_only) @@ -1103,30 +1077,31 @@ def _cell_num_vertices(cls) -> pd.PositiveInt: @classmethod @requires_vtk - def _vtk_cell_type(cls) -> VtkCellType: + def _vtk_cell_type(cls): """VTK cell type to use in the VTK representation.""" - return VTK_TRIANGLE + return vtk["mod"].VTK_TRIANGLE @classmethod @requires_vtk - def _from_vtk_obj(cls, vtk_obj: Union[vtkPolyData, vtkUnstructuredGrid]): + def _from_vtk_obj(cls, vtk_obj): """Initialize from a vtkUnstructuredGrid instance.""" # get points cells data from vtk object - if isinstance(vtk_obj, vtkPolyData): + if isinstance(vtk_obj, vtk["mod"].vtkPolyData): cells_vtk = vtk_obj.GetPolys() - elif isinstance(vtk_obj, vtkUnstructuredGrid): + elif isinstance(vtk_obj, vtk["mod"].vtkUnstructuredGrid): cells_vtk = vtk_obj.GetCells() - cells_numpy = vtk_to_numpy(cells_vtk.GetConnectivityArray()) + cells_numpy = vtk["vtk_to_numpy"](cells_vtk.GetConnectivityArray()) - cell_offsets = vtk_to_numpy(cells_vtk.GetOffsetsArray()) + 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'." + "Only triangular 'vtkUnstructuredGrid' or 'vtkPolyData' can be converted into " + "'TriangularGridDataset'." ) - points_numpy = vtk_to_numpy(vtk_obj.GetPoints().GetData()) + 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)) @@ -1197,7 +1172,7 @@ def plane_slice(self, axis: Axis, pos: float) -> SpatialDataArray: # perform slicing in vtk and get unprocessed points and values slice_vtk = self._plane_slice_raw(axis=axis, pos=pos) - points_numpy = vtk_to_numpy(slice_vtk.GetPoints().GetData()) + 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 @@ -1490,22 +1465,22 @@ def _cell_num_vertices(cls) -> pd.PositiveInt: @classmethod @requires_vtk - def _vtk_cell_type(cls) -> VtkCellType: + def _vtk_cell_type(cls): """VTK cell type to use in the VTK representation.""" - return VTK_TETRA + return vtk["mod"].VTK_TETRA @classmethod @requires_vtk - def _from_vtk_obj(cls, grid: vtkUnstructuredGrid) -> TetrahedralGridDataset: + 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_to_numpy(grid.GetCells().GetConnectivityArray()) - points_numpy = vtk_to_numpy(grid.GetPoints().GetData()) + 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_to_numpy(grid.GetCellTypesArray()) + 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") @@ -1573,7 +1548,7 @@ def line_slice(self, axis: Axis, pos: Coordinate) -> SpatialDataArray: end[axis] = bounds[1][axis] # create cutting plane - line = vtkLineSource() + line = vtk["mod"].vtkLineSource() line.SetPoint1(start) line.SetPoint2(end) line.SetResolution(1) @@ -1585,7 +1560,7 @@ def line_slice(self, axis: Axis, pos: Coordinate) -> SpatialDataArray: # 2) do plane slice along first direction # 3) do second plane slice along second direction - prober = vtkExtractCellsAlongPolyLine() + prober = vtk["mod"].vtkExtractCellsAlongPolyLine() prober.SetSourceConnection(line.GetOutputPort()) prober.SetInputData(self._vtk_obj) prober.Update() diff --git a/tidy3d/components/types.py b/tidy3d/components/types.py index cb8f2094c..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, Any +import functools # Literal only available in python 3.8 + so try import otherwise use extensions try: @@ -13,7 +14,7 @@ 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: @@ -21,17 +22,47 @@ except ImportError: trimesh = None -try: - import vtk - from vtkmodules.vtkCommonCore import vtkLogger +vtk = { + "mod": None, + "id_type": np.int64, + "vtk_to_numpy": None, + "numpy_to_vtkIdTypeArray": None, + "numpy_to_vtk": None, +} - vtkLogger.SetStderrVerbosity(vtkLogger.VERBOSITY_WARNING) - vtk_id_type = np.int32 if vtk.vtkIdTypeArray().GetDataTypeSize() == 4 else np.int64 +def requires_vtk(fn): + """When decorating a method, requires that vtk is available.""" -except ImportError: - vtk = None - vtk_id_type = np.int64 + @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 @@ -259,7 +290,3 @@ def __modify_schema__(cls, field_schema): """ mode tracking """ TrackFreq = Literal["central", "lowest", "highest"] - -""" VTK """ - -VtkCellType = Any if vtk is None else Literal[vtk.VTK_TRIANGLE, vtk.VTK_TETRA] From 08ea7432fc69c3e50a03bb94198b6ac799b11bd5 Mon Sep 17 00:00:00 2001 From: momchil Date: Wed, 6 Dec 2023 11:28:36 -0800 Subject: [PATCH 70/83] Return 0 spectrum if no times are above the dft cutoff --- tidy3d/components/time.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tidy3d/components/time.py b/tidy3d/components/time.py index d7588bd92..1c518b0bc 100644 --- a/tidy3d/components/time.py +++ b/tidy3d/components/time.py @@ -79,7 +79,7 @@ def spectrum( time_amps = np.real(time_amps) # if all time amplitudes are zero, just return (complex-valued) zeros for spectrum - if np.allclose(time_amps, 0.0): + if np.all(np.equal(time_amps, 0.0)): return (0.0 + 0.0j) * np.zeros_like(freqs) # Cut to only relevant times @@ -89,6 +89,8 @@ def spectrum( stop_ind = relevant_time_inds[0][-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 From aee23aa727738d54da82c1e64d9144c02df67708 Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Wed, 6 Dec 2023 09:54:05 -0800 Subject: [PATCH 71/83] Rename Silicon in material library --- tidy3d/material_library/material_library.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, From 065831ad6fdd556baa9d02845969c0567ed83317 Mon Sep 17 00:00:00 2001 From: momchil Date: Thu, 7 Dec 2023 13:57:43 -0800 Subject: [PATCH 72/83] Upating version to 2.5.0 --- tidy3d/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tidy3d/version.py b/tidy3d/version.py index ca985b250..2cc89c4ac 100644 --- a/tidy3d/version.py +++ b/tidy3d/version.py @@ -1,3 +1,3 @@ """Defines the front end version of tidy3d""" -__version__ = "2.5.0rc3" +__version__ = "2.5.0" From e9fc81b70a34a64ce0c4c283aced8b6032cc57af Mon Sep 17 00:00:00 2001 From: Shashwat Sharma Date: Tue, 5 Dec 2023 20:14:42 -0500 Subject: [PATCH 73/83] add ability to project fields through a manually specified material --- CHANGELOG.md | 1 + tidy3d/components/field_projection.py | 40 +++++++++++++++++---------- tidy3d/components/monitor.py | 11 ++++++++ 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f0e928f2..79906e650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### 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. diff --git a/tidy3d/components/field_projection.py b/tidy3d/components/field_projection.py index 6de57add0..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 @@ -546,7 +548,8 @@ 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) ) @@ -564,6 +567,7 @@ def _project_fields_angular( phi=phi, surface=surface, currents=currents, + medium=medium, ) for field, _field in zip(fields, _fields): field[..., idx_f] += _field * phase[idx_f] @@ -580,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=_x, y=_y, z=_z, surface=surface, currents=currents + x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium ) for field, _field in zip(fields, _fields): field[0, i, j, :] += _field @@ -591,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( @@ -622,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 = [ @@ -655,12 +660,13 @@ def _project_fields_cartesian( 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=_x, y=_y, z=_z, surface=surface, currents=currents + x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium ) for field, _field in zip(fields, _fields): field[i, j, k, :] += _field @@ -671,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( @@ -698,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) ) @@ -726,6 +733,7 @@ def _project_fields_kspace( 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] @@ -733,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=_x, y=_y, z=_z, surface=surface, currents=currents + x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium ) for field, _field in zip(fields, _fields): field[i, j, 0, :] += _field @@ -749,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""" @@ -761,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 @@ -778,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 ------- @@ -785,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/monitor.py b/tidy3d/components/monitor.py index 8b709b9c2..f8007d2fa 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -11,6 +11,7 @@ from .base import cached_property, Tidy3dBaseModel from .mode import ModeSpec from .apodization import ApodizationSpec +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 @@ -685,6 +686,16 @@ class AbstractFieldProjectionMonitor(SurfaceIntegrationMonitor, FreqMonitor): "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.""" From b3e40653a054a3fe4548e064f4d1420e6295ab2d Mon Sep 17 00:00:00 2001 From: momchil Date: Fri, 8 Dec 2023 15:32:15 -0800 Subject: [PATCH 74/83] Validate that ModeSolver.plane intersects ModeSolver.simulation --- tidy3d/plugins/mode/mode_solver.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 0a875ff38..b9e0c41e5 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -98,6 +98,17 @@ def is_plane(cls, val): _freqs_not_empty = validate_freqs_not_empty() _freqs_lower_bound = validate_freqs_min() + @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 def normal_axis(self) -> Axis: """Axis normal to the mode plane.""" From 5c63398c5660cbe6839d6f2950c25d3037cf18d0 Mon Sep 17 00:00:00 2001 From: momchil Date: Sat, 9 Dec 2023 19:06:19 -0800 Subject: [PATCH 75/83] Allow bend_radius to be negative, but not close to zero --- tidy3d/components/mode.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tidy3d/components/mode.py b/tidy3d/components/mode.py index 0620a30e9..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 @@ -77,7 +78,7 @@ class ModeSpec(Tidy3dBaseModel): "single precision, but more accurate under double precision.", ) - bend_radius: pd.PositiveFloat = pd.Field( + bend_radius: float = pd.Field( None, title="Bend radius", description="A curvature radius for simulation of waveguide bends. Can be negative, in " @@ -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) From 259d1c4b48284a1128ea472de9e892ad8e662c3c Mon Sep 17 00:00:00 2001 From: Casey Wojcik Date: Fri, 1 Dec 2023 11:59:08 +0000 Subject: [PATCH 76/83] Changed flux and DFT definitions with complex fields to only use real part, and fixed TFSF flux leakage bug --- CHANGELOG.md | 2 ++ tests/sims/simulation_2_5_0rc3.h5 | Bin 459872 -> 0 bytes tidy3d/components/data/monitor_data.py | 4 ++-- tidy3d/components/data/sim_data.py | 10 +--------- tidy3d/components/medium.py | 20 ++++++++++++++++++++ tidy3d/components/simulation.py | 4 ++++ tidy3d/components/source.py | 6 ++---- tidy3d/components/time.py | 21 +++++++-------------- 8 files changed, 38 insertions(+), 29 deletions(-) delete mode 100644 tests/sims/simulation_2_5_0rc3.h5 diff --git a/CHANGELOG.md b/CHANGELOG.md index 79906e650..075f1e195 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,8 +16,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. ### Fixed +- Fixed energy leakage in TFSF when using complex fields. ## [2.5.0rc3] - 2023-11-30 diff --git a/tests/sims/simulation_2_5_0rc3.h5 b/tests/sims/simulation_2_5_0rc3.h5 deleted file mode 100644 index fed60dfa99be8f425a10e019a319fbd20c8cb3a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 459872 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͋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 diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index f7f28557e..9fd1a3292 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -843,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 diff --git a/tidy3d/components/data/sim_data.py b/tidy3d/components/data/sim_data.py index 83abfd669..101c840c2 100644 --- a/tidy3d/components/data/sim_data.py +++ b/tidy3d/components/data/sim_data.py @@ -11,8 +11,6 @@ from .monitor_data import MonitorDataTypes, MonitorDataType, AbstractFieldData, FieldTimeData 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 @@ -116,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. diff --git a/tidy3d/components/medium.py b/tidy3d/components/medium.py index bd34a0b35..160a743d2 100644 --- a/tidy3d/components/medium.py +++ b/tidy3d/components/medium.py @@ -155,6 +155,11 @@ def _get_n0( ) return n0 + @property + def complex_fields(self) -> bool: + """Whether the model uses complex fields.""" + pass + class NonlinearSusceptibility(NonlinearModel): """Model for an instantaneous nonlinear chi3 susceptibility. @@ -215,6 +220,11 @@ def _validate_numiters(cls, val): ) return val + @property + def complex_fields(self) -> bool: + """Whether the model uses complex fields.""" + return False + class TwoPhotonAbsorption(NonlinearModel): """Model for two-photon absorption (TPA) nonlinearity which gives an intensity-dependent @@ -287,6 +297,11 @@ def _validate_medium(self, medium: AbstractMedium): 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 @@ -358,6 +373,11 @@ def _validate_medium(self, medium: AbstractMedium): 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] diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index e1877f4a6..0bb5c2134 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -2403,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 diff --git a/tidy3d/components/source.py b/tidy3d/components/source.py index b2d150bce..406e9dde8 100644 --- a/tidy3d/components/source.py +++ b/tidy3d/components/source.py @@ -46,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 ---------- @@ -61,8 +61,6 @@ 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 ------- @@ -72,7 +70,7 @@ def plot_spectrum( fmin, fmax = self.frequency_range() return self.plot_spectrum_in_frequency_range( - times, fmin, fmax, num_freqs=num_freqs, val=val, ax=ax, complex_fields=complex_fields + times, fmin, fmax, num_freqs=num_freqs, val=val, ax=ax ) @abstractmethod diff --git a/tidy3d/components/time.py b/tidy3d/components/time.py index 1c518b0bc..1117ec6dd 100644 --- a/tidy3d/components/time.py +++ b/tidy3d/components/time.py @@ -40,7 +40,7 @@ def amp_time(self, time: float) -> complex: Returns ------- complex - Complex-valued amplitude at that time.. + Complex-valued amplitude at that time. """ def spectrum( @@ -48,9 +48,9 @@ def spectrum( times: ArrayFloat1D, freqs: ArrayFloat1D, dt: float, - complex_fields: bool = False, ) -> complex: - """Complex-valued spectrum as a function of frequency + """Complex-valued spectrum as a function of frequency. + Note: Only the real part of the time signal is used. Parameters ---------- @@ -62,8 +62,6 @@ def spectrum( 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 ------- @@ -73,10 +71,7 @@ def spectrum( times = np.array(times) freqs = np.array(freqs) - time_amps = self.amp_time(times) - - if not complex_fields: - time_amps = np.real(time_amps) + 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)): @@ -86,7 +81,7 @@ def spectrum( 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] + 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: @@ -118,9 +113,9 @@ def plot_spectrum_in_frequency_range( num_freqs: int = 101, val: PlotVal = "real", ax: Ax = None, - complex_fields: bool = False, ) -> Ax: """Plot the complex-valued amplitude of the time-dependence. + Note: Only the real part of the time signal is used. Parameters ---------- @@ -137,8 +132,6 @@ def plot_spectrum_in_frequency_range( 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. - complex_fields : bool - Whether time domain fields are complex, e.g., for Bloch boundaries Returns ------- @@ -154,7 +147,7 @@ def plot_spectrum_in_frequency_range( dt = np.mean(dts) freqs = np.linspace(fmin, fmax, num_freqs) - spectrum = self.spectrum(times=times, dt=dt, freqs=freqs, complex_fields=complex_fields) + spectrum = self.spectrum(times=times, dt=dt, freqs=freqs) if val == "real": ax.plot(freqs, spectrum.real, color="blueviolet", label="real") From e820311380d70b3d9110302d82a80616724ddec8 Mon Sep 17 00:00:00 2001 From: momchil Date: Fri, 8 Dec 2023 10:57:21 -0800 Subject: [PATCH 77/83] Convenience property in mode solver to get number of cells, freqs, and modes --- tidy3d/plugins/mode/mode_solver.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index b9e0c41e5..019db070c 100644 --- a/tidy3d/plugins/mode/mode_solver.py +++ b/tidy3d/plugins/mode/mode_solver.py @@ -153,6 +153,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. From e176ec5c5dffd2425009b18d4656bd595f84283a Mon Sep 17 00:00:00 2001 From: momchil Date: Tue, 12 Dec 2023 09:17:50 -0800 Subject: [PATCH 78/83] Doubling mode solver internal storage for doulbe precision --- tidy3d/components/monitor.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index f8007d2fa..b33f9dca1 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -317,7 +317,10 @@ def _warn_num_modes(cls, val, 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 mode surface - return BYTES_COMPLEX * num_cells * len(self.freqs) * self.mode_spec.num_modes * 6 + 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): From ca2052555d7dc6b0d8d9b4c82a03abf5138d1d8a Mon Sep 17 00:00:00 2001 From: momchil Date: Fri, 8 Dec 2023 15:02:56 -0800 Subject: [PATCH 79/83] Print cost breakdown in estimate_cost if there is mode or post-processing charge --- tidy3d/web/api/webapi.py | 6 ++++++ tidy3d/web/core/task_info.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/tidy3d/web/api/webapi.py b/tidy3d/web/api/webapi.py index 26b4b0cf2..9b7a796ce 100644 --- a/tidy3d/web/api/webapi.py +++ b/tidy3d/web/api/webapi.py @@ -760,6 +760,12 @@ def estimate_cost(task_id: str, verbose: bool = True) -> float: "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( diff --git a/tidy3d/web/core/task_info.py b/tidy3d/web/core/task_info.py index a8a74f336..f1714db3a 100644 --- a/tidy3d/web/core/task_info.py +++ b/tidy3d/web/core/task_info.py @@ -62,6 +62,9 @@ class TaskInfo(TaskBase): 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 From 523df255a01a1267957ece43af1b586fb4f9607f Mon Sep 17 00:00:00 2001 From: momchil Date: Tue, 12 Dec 2023 12:01:01 -0800 Subject: [PATCH 80/83] Mode solver eps_complex check at central frequency only --- tidy3d/plugins/mode/mode_solver.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tidy3d/plugins/mode/mode_solver.py b/tidy3d/plugins/mode/mode_solver.py index 019db070c..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 @@ -642,14 +643,16 @@ def _has_fully_anisotropic_media(self) -> bool: @cached_property def _has_complex_eps(self) -> bool: - """Check if there are media with a complex-valued epsilon in the plane of the mode at the - mode solver freqs. A separate check is done inside the solver, which looks at the actual + """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: - max_imag_eps = np.amax(np.abs(np.imag(int_mat.eps_model(np.array(self.freqs))))) - if not np.isclose(max_imag_eps, 0): - return False + 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( From 08578216299801b3c54bf5a31da3d02b11ee1765 Mon Sep 17 00:00:00 2001 From: dbochkov-flexcompute Date: Tue, 12 Dec 2023 18:48:09 -0600 Subject: [PATCH 81/83] limit vtk version --- requirements/vtk.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/vtk.txt b/requirements/vtk.txt index 4f7e3c87b..a4cde2118 100644 --- a/requirements/vtk.txt +++ b/requirements/vtk.txt @@ -1,4 +1,4 @@ # vtk -vtk +vtk<=9.2.6 From c34a23e3d6433b4ad17d824ef67be166146a8afb Mon Sep 17 00:00:00 2001 From: Casey Wojcik Date: Wed, 13 Dec 2023 12:27:03 +0000 Subject: [PATCH 82/83] Added max_num_poles at least min_num_poles validator to FastDispersionFitter --- tidy3d/plugins/dispersion/fit_fast.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) 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] From 46ecef4d9e4d8b552cdf5a1aab6db8aec19862de Mon Sep 17 00:00:00 2001 From: momchil Date: Wed, 13 Dec 2023 13:35:34 -0800 Subject: [PATCH 83/83] Final changelog and schema for 2.5.0 --- CHANGELOG.md | 77 +- tests/sims/simulation_2_5_0.h5 | Bin 0 -> 460416 bytes tests/sims/simulation_2_5_0.json | 2274 ++++++++++++++++++++++++++++++ tidy3d/schema.json | 262 +++- 4 files changed, 2558 insertions(+), 55 deletions(-) create mode 100644 tests/sims/simulation_2_5_0.h5 create mode 100644 tests/sims/simulation_2_5_0.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 075f1e195..a45c67b20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,25 +5,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [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. - -### 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. - -### Fixed -- Fixed energy leakage in TFSF when using complex fields. - -## [2.5.0rc3] - 2023-11-30 - -### Added - 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. @@ -36,16 +25,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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. @@ -57,40 +69,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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"`. - -## [2.5.0rc2] - 2023-10-30 - -### Added -- 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. - -### Changed -- 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. - -### Fixed - 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.5.0rc1] - 2023-10-10 - -### Added -- 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 -- Internal refactor of Web API functionality. -- `Geometry.from_gds` doesn't create unnecessary groups of single elements. - ## [2.4.3] - 2023-10-16 ### Added @@ -1059,11 +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.5.0rc3...pre/2.5 -[2.5.0rc3]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc2...v2.5.0rc3 -[2.5.0rc2]: https://github.com/flexcompute/tidy3d/compare/v2.5.0rc1...v2.5.0rc2 -[2.5.0rc1]: https://github.com/flexcompute/tidy3d/compare/v2.4.2...v2.5.0rc1 -[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/tests/sims/simulation_2_5_0.h5 b/tests/sims/simulation_2_5_0.h5 new file mode 100644 index 0000000000000000000000000000000000000000..03ea92ea70d2f0e20f50c99bcda6253e1afaabec GIT binary patch literal 460416 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 M8MFPyyA1aK0a?AG)&Kwi literal 0 HcmV?d00001 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/tidy3d/schema.json b/tidy3d/schema.json index 4581a0d10..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.\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.0rc3\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... )", + "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": { @@ -475,7 +475,7 @@ "version": { "title": "Version", "description": "String specifying the front end version number.", - "default": "2.5.0rc3", + "default": "2.5.0", "type": "string" }, "run_time": { @@ -8468,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[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.\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": { @@ -8647,6 +8647,90 @@ "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_distance": { "title": "Projection Distance", "description": "Radial distance of the projection points from ``local_origin``.", @@ -8698,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[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.\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": { @@ -8877,6 +8961,90 @@ "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", "description": "Axis along which the observation plane is oriented.", @@ -8939,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[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.\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": { @@ -9118,6 +9286,90 @@ "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", "description": "Axis along which the observation plane is oriented.",