From f4654967bc99f315929cca2de09520648994cad3 Mon Sep 17 00:00:00 2001 From: Qiming <qiming@flexcompute.com> Date: Fri, 13 Sep 2024 17:36:45 -0400 Subject: [PATCH] Error if projection monitor touches periodic/bloch boundaries in 3D simulations --- CHANGELOG.md | 3 ++ tests/test_components/test_simulation.py | 38 ++++++++++++++++++++-- tests/utils.py | 16 ++++----- tidy3d/components/simulation.py | 41 ++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bce65683f..5bbed77ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- Validator `boundaries_for_proj_mnts` to error when projection monitor touches periodic/bloch boundaries in 3D simulations. + ## [2.7.3] - 2024-09-12 ### Added diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index b68dea733..318954af1 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -1012,7 +1012,7 @@ def test_proj_monitor_distance(log_capture): monitor_n2f = td.FieldProjectionAngleMonitor( center=(0, 0, 0), - size=(td.inf, td.inf, 0), + size=(0.99, 0.99, 0), freqs=[250e12, 300e12], name="monitor_n2f", theta=[0], @@ -1023,7 +1023,7 @@ def test_proj_monitor_distance(log_capture): monitor_n2f_far = td.FieldProjectionAngleMonitor( center=(0, 0, 0), - size=(td.inf, td.inf, 0), + size=(0.99, 0.99, 0), freqs=[250e12, 300e12], name="monitor_n2f", theta=[0], @@ -1034,7 +1034,7 @@ def test_proj_monitor_distance(log_capture): monitor_n2f_approx = td.FieldProjectionAngleMonitor( center=(0, 0, 0), - size=(td.inf, td.inf, 0), + size=(0.99, 0.99, 0), freqs=[250e12, 300e12], name="monitor_n2f", theta=[0], @@ -1783,6 +1783,38 @@ def test_tfsf_structures_grid(log_capture): sim.validate_pre_upload() +def test_error_bloch_proj_mnts(): + """Test if Bloch/periodic boundaries touching projection monitors raise an error in 3D simulations.""" + + monitor_n2f = td.FieldProjectionAngleMonitor( + center=(0, 0, 0), + size=(2, 2, 0), + freqs=[2.5e14], + name="monitor_n2f", + theta=[0], + phi=[0], + proj_distance=1e5, + ) + with pytest.raises(pydantic.ValidationError): + _ = td.Simulation( + size=(2, 2, 2), + structures=[], + sources=[ + td.PointDipole( + center=(0, 0, 0), + polarization="Ex", + source_time=td.GaussianPulse( + freq0=1e14, + fwidth=1e12, + ), + ) + ], + run_time=1e-12, + boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()), + monitors=[monitor_n2f], + ) + + @pytest.mark.parametrize( "size, num_struct, log_level", [(1, 1, None), (50, 1, "WARNING"), (1, 11000, "WARNING")] ) diff --git a/tests/utils.py b/tests/utils.py index 16f45f6a6..aba3aaafa 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -790,8 +790,8 @@ def make_custom_data(lims, unstructured): mode_spec=td.ModeSpec(), ), td.FieldProjectionAngleMonitor( - center=(0, 0, 0), - size=(0, 2, 2), + center=(0.1, 0.05, 0.02), + size=(0, 0.49, 0.69), freqs=[250e12, 300e12], name="proj_angle", custom_origin=(1, 2, 3), @@ -799,8 +799,8 @@ def make_custom_data(lims, unstructured): theta=np.linspace(np.pi / 4, np.pi / 4 + np.pi / 2, 100), ), td.FieldProjectionCartesianMonitor( - center=(0, 0, 0), - size=(0, 2, 2), + center=(0.1, 0.05, 0.02), + size=(0, 0.49, 0.69), freqs=[250e12, 300e12], name="proj_cartesian", custom_origin=(1, 2, 3), @@ -810,8 +810,8 @@ def make_custom_data(lims, unstructured): proj_distance=5, ), td.FieldProjectionKSpaceMonitor( - center=(0, 0, 0), - size=(0, 2, 2), + center=(0.1, 0.05, 0.02), + size=(0, 0.49, 0.69), freqs=[250e12, 300e12], name="proj_kspace", custom_origin=(1, 2, 3), @@ -820,8 +820,8 @@ def make_custom_data(lims, unstructured): uy=[0.03, 0.04, 0.05], ), td.FieldProjectionAngleMonitor( - center=(0, 0, 0), - size=(0, 2, 2), + center=(0.1, 0.05, 0.02), + size=(0, 0.49, 0.69), freqs=[250e12, 300e12], name="proj_angle_exact", custom_origin=(1, 2, 3), diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index b64c3f26e..db11e2e8a 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -2494,6 +2494,47 @@ def boundaries_for_zero_dims(cls, val, values): return val + @pydantic.validator("monitors", always=True) + @skip_if_fields_missing(["size", "boundary_spec"]) + def boundaries_for_proj_mnts(cls, val, values): + """Error if periodic boundaries or bloch boundaries touch field projection monitors in 3D simulations.""" + + if val is None: + return val + + sim_size = values.get("size") + sim_center = values.get("center") + boundaries = values.get("boundary_spec").to_list + + # Simulation domain as a Box + simulation_box = Box(size=sim_size, center=sim_center) + + axis_names = "xyz" + + for dim, boundary in enumerate(boundaries): + axis = axis_names[dim] + num_periodic_bloch_boundaries = sum( + isinstance(bnd, (Periodic, BlochBoundary)) for bnd in boundary + ) + + if num_periodic_bloch_boundaries > 0: + for monitor in val: + if ( + isinstance(monitor, AbstractFieldProjectionMonitor) + and sim_size.count(0.0) == 0 + ): + if ( + monitor.bounds[0][dim] <= simulation_box.bounds[0][dim] + or monitor.bounds[1][dim] >= simulation_box.bounds[1][dim] + ): + raise SetupError( + f"The '{monitor.name}' monitor touches periodic/bloch boundaries " + f"along the '{axis}' axis. This would lead to incorrect results. " + f"Please adjust the monitor size to fit within the simulation domain." + ) + + return val + @pydantic.validator("sources", always=True) def _validate_num_sources(cls, val): """Error if too many sources present."""