From 0957b37d321111a970f8e9f4fdd1ca82f65d6ab8 Mon Sep 17 00:00:00 2001 From: tylerflex Date: Tue, 14 Dec 2021 11:49:20 -0800 Subject: [PATCH 1/5] added warnings for source / medium frq ranges and grid size --- tests/test_components.py | 73 ++++++++++++++++++++++++++++++++- tidy3d/components/simulation.py | 70 +++++++++++++++++++++++++++---- tidy3d/components/validators.py | 10 ++--- 3 files changed, 138 insertions(+), 15 deletions(-) diff --git a/tests/test_components.py b/tests/test_components.py index 2fe67ff606..948f3621f7 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -116,7 +116,78 @@ def place_box(center_offset): def test_sim_grid_size(): size = (1, 1, 1) - s = Simulation(size=size, grid_size=(1.0, 1.0, 1.0)) + _ = Simulation(size=size, grid_size=(1.0, 1.0, 1.0)) + +def assert_log_level(caplog, log_level_expected): + """ ensure something got logged if log_level is not None""" + + # get log output + logs = caplog.record_tuples + + # there's a log but the log level is not None (problem) + if logs and not log_level_expected: + raise Exception + + # we expect a log but none is given (problem) + if log_level_expected and not logs: + raise Exception + + # both expected and got log, check the log levels match + if logs and log_level_expected: + for log in logs: + log_level = log[1] + if log_level == log_level_expected: + # log level was triggered, exit + return + raise Exception + + + +@pytest.mark.parametrize("fwidth,log_level", [(0.001, None), (3, 30)]) +def test_sim_frequency_range(caplog, fwidth, log_level): + # small fwidth should be inside range, large one should throw warning + + size = (1, 1, 1) + medium = Medium(frequency_range=(2, 3)) + box = Structure( + geometry=Box(size=(.1, .1, .1)), + medium=medium) + src = VolumeSource( + source_time=GaussianPulse(freq0=2.4, fwidth=fwidth), + size=(0,0,0), + polarization='Ex', + ) + _ = Simulation( + size=(1, 1, 1), + grid_size=(.1, .1, .1), + structures=[box], + sources=[src]) + + assert_log_level(caplog, log_level) + + +@pytest.mark.parametrize("grid_size,log_level", [(0.001, None), (3, 30)]) +def test_sim_grid_size(caplog, grid_size, log_level): + # small fwidth should be inside range, large one should throw warning + + size = (1, 1, 1) + medium = Medium(permittivity=2, frequency_range=(2e14, 3e14)) + box = Structure( + geometry=Box(size=(.1, .1, .1)), + medium=medium) + src = VolumeSource( + source_time=GaussianPulse(freq0=2.5e14, fwidth=1e13), + size=(0,0,0), + polarization='Ex', + ) + _ = Simulation( + size=(1, 1, 1), + grid_size=(.1, .1, grid_size), + structures=[box], + sources=[src]) + + assert_log_level(caplog, log_level) + """ geometry """ diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 55d9874da9..28be59b8bd 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -30,6 +30,9 @@ # technically this is creating a circular import issue because it calls tidy3d/__init__.py # from .. import __version__ as version_number +# minimum number of grid points allowed per central wavelength in a medium +MIN_GRIDS_PER_WVL = 6.0 + class Simulation(Box): # pylint:disable=too-many-public-methods # pylint:disable=line-too-long @@ -169,9 +172,8 @@ def set_none_to_zero_layers(cls, val): _monitors_in_bounds = assert_objects_in_sim_bounds("monitors") # assign names to unnamed structures, sources, and mediums - _structure_names = set_names("structures") - _source_names = set_names("sources") - + # _structure_names = set_names("structures") + # _source_names = set_names("sources") # @pydantic.validator("structures", allow_reuse=True, always=True) # def set_medium_names(cls, val, values): # """check for intersection of each structure with simulation bounds.""" @@ -189,12 +191,62 @@ def set_none_to_zero_layers(cls, val): _unique_monitor_names = assert_unique_names("monitors") # _unique_medium_names = assert_unique_names("structures", check_mediums=True) - # TODO: - # - check sources in medium freq range - # - check PW in homogeneous medium - # - check nonuniform grid covers the whole simulation domain - # - check any structures close to PML (in lambda) without intersecting. - # - check any structures.bounds == simulation.bounds -> warn + @pydantic.validator("sources", always=True) + def _warn_sources_mediums_frequency_range(cls, val, values): + """Warn user if any sources have frequency range outside of medium frequency range.""" + structures = values.get("structures") + medium_bg = values.get("medium") + mediums = [medium_bg] + [structure.medium for structure in structures] + + for source in val: + fmin_src, fmax_src = source.source_time.frequency_range + for medium in mediums: + + # skip mediums that have no freq range (all freqs valid) + if medium.frequency_range is None: + continue + + # make sure medium frequency range includes the source frequency range + fmin_med, fmax_med = medium.frequency_range + if fmin_med > fmin_src or fmax_med < fmax_src: + log.warning(f"A medium in the simulation:\n\n({medium})\n\nhas a frequency " + "range that does not fully cover the spetrum of a source:" + f"\n\n({source})\n\nThis can cause innacuracies in the " + "simulation results.") + return val + + @pydantic.validator("sources", always=True) + def _warn_grid_size_too_small(cls, val, values): + """Warn user if any sources have frequency range outside of medium frequency range.""" + structures = values.get("structures") + medium_bg = values.get("medium") + grid_size = values.get("grid_size") + mediums = [medium_bg] + [structure.medium for structure in structures] + + for source in val: + fmin_src, fmax_src = source.source_time.frequency_range + f_average = (fmin_src + fmax_src) / 2.0 + + for medium in mediums: + + eps_material = medium.eps_model(f_average) + n_material, _ = medium.eps_complex_to_nk(eps_material) + lambda_min = C_0 / f_average / n_material + + for dl in grid_size: + if isinstance(dl, float): + if dl > lambda_min / MIN_GRIDS_PER_WVL: + log.warning(f"One of the grid sizes with a value of {dl:.4f} (um) " + "was detected as being too large when compared to the " + "central wavelength of a source within one of the " + f"simulation mediums {lambda_min:.4f} (um). " + "To avoid inaccuracies, it is reccomended the grid size is " + "reduced." + ) + + return val + + """ Accounting """ diff --git a/tidy3d/components/validators.py b/tidy3d/components/validators.py index 8657dc1d42..428b8a50d4 100644 --- a/tidy3d/components/validators.py +++ b/tidy3d/components/validators.py @@ -40,7 +40,7 @@ def assert_plane(): - """makes sure a field's `size` attribute has exactly 1 zero""" + """Makes sure a field's `size` attribute has exactly 1 zero.""" @pydantic.validator("size", allow_reuse=True, always=True) def is_plane(cls, val): @@ -52,7 +52,7 @@ def is_plane(cls, val): def validate_name_str(): - """make sure the name doesnt include [, ] (used for default names)""" + """Make sure the name doesnt include [, ] (used for default names).""" @pydantic.validator("name", allow_reuse=True, always=True, pre=True) def field_has_unique_names(cls, val): @@ -65,7 +65,7 @@ def field_has_unique_names(cls, val): def assert_unique_names(field_name: str, check_mediums=False): - """makes sure all elements of a field have unique .name values""" + """Makes sure all elements of a field have unique .name values.""" @pydantic.validator(field_name, allow_reuse=True, always=True) def field_has_unique_names(cls, val, values): @@ -85,7 +85,7 @@ def field_has_unique_names(cls, val, values): def assert_objects_in_sim_bounds(field_name: str): - """makes sure all objects in field are at least partially inside of simulation bounds/""" + """Makes sure all objects in field are at least partially inside of simulation bounds.""" @pydantic.validator(field_name, allow_reuse=True, always=True) def objects_in_sim_bounds(cls, val, values): @@ -104,7 +104,7 @@ def objects_in_sim_bounds(cls, val, values): def set_names(field_name: str): - """set names""" + """Set names.""" @pydantic.validator(field_name, allow_reuse=True, always=True) def set_unique_names(cls, val): From 304f7d258f80e403b9ad44aa46b8eb4de98e305d Mon Sep 17 00:00:00 2001 From: tylerflex Date: Tue, 14 Dec 2021 13:36:54 -0800 Subject: [PATCH 2/5] added plane wave homogeneous medium check --- tests/test_components.py | 73 +++++++++++++-------- tests/test_grid.py | 34 ++++++---- tidy3d/components/simulation.py | 109 ++++++++++++++++++-------------- 3 files changed, 131 insertions(+), 85 deletions(-) diff --git a/tests/test_components.py b/tests/test_components.py index 948f3621f7..ada17fa101 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -118,8 +118,9 @@ def test_sim_grid_size(): size = (1, 1, 1) _ = Simulation(size=size, grid_size=(1.0, 1.0, 1.0)) + def assert_log_level(caplog, log_level_expected): - """ ensure something got logged if log_level is not None""" + """ensure something got logged if log_level is not None""" # get log output logs = caplog.record_tuples @@ -142,27 +143,20 @@ def assert_log_level(caplog, log_level_expected): raise Exception - @pytest.mark.parametrize("fwidth,log_level", [(0.001, None), (3, 30)]) def test_sim_frequency_range(caplog, fwidth, log_level): # small fwidth should be inside range, large one should throw warning size = (1, 1, 1) medium = Medium(frequency_range=(2, 3)) - box = Structure( - geometry=Box(size=(.1, .1, .1)), - medium=medium) + box = Structure(geometry=Box(size=(0.1, 0.1, 0.1)), medium=medium) src = VolumeSource( source_time=GaussianPulse(freq0=2.4, fwidth=fwidth), - size=(0,0,0), - polarization='Ex', + size=(0, 0, 0), + polarization="Ex", ) - _ = Simulation( - size=(1, 1, 1), - grid_size=(.1, .1, .1), - structures=[box], - sources=[src]) - + _ = Simulation(size=(1, 1, 1), grid_size=(0.1, 0.1, 0.1), structures=[box], sources=[src]) + assert_log_level(caplog, log_level) @@ -170,25 +164,54 @@ def test_sim_frequency_range(caplog, fwidth, log_level): def test_sim_grid_size(caplog, grid_size, log_level): # small fwidth should be inside range, large one should throw warning - size = (1, 1, 1) medium = Medium(permittivity=2, frequency_range=(2e14, 3e14)) - box = Structure( - geometry=Box(size=(.1, .1, .1)), - medium=medium) + box = Structure(geometry=Box(size=(0.1, 0.1, 0.1)), medium=medium) src = VolumeSource( source_time=GaussianPulse(freq0=2.5e14, fwidth=1e13), - size=(0,0,0), - polarization='Ex', + size=(0, 0, 0), + polarization="Ex", ) - _ = Simulation( - size=(1, 1, 1), - grid_size=(.1, .1, grid_size), - structures=[box], - sources=[src]) + _ = Simulation(size=(1, 1, 1), grid_size=(0.1, 0.1, grid_size), structures=[box], sources=[src]) assert_log_level(caplog, log_level) +def test_sim_plane_wave_error(): + # small fwidth should be inside range, large one should throw warning + + medium_bg = Medium(permittivity=2) + medium_air = Medium(permittivity=1) + + box = Structure(geometry=Box(size=(0.1, 0.1, 0.1)), medium=medium_air) + + box_transparent = Structure(geometry=Box(size=(0.1, 0.1, 0.1)), medium=medium_bg) + + src = PlaneWave( + source_time=GaussianPulse(freq0=2.5e14, fwidth=1e13), + center=(0, 0, 0), + size=(inf, inf, 0), + direction="+", + polarization="Ex", + ) + + # with transparent box continue + _ = Simulation( + size=(1, 1, 1), + grid_size=(0.1, 0.1, 0.1), + medium=medium_bg, + structures=[box_transparent], + sources=[src], + ) + + # with non-transparent box, raise + with pytest.raises(SetupError): + _ = Simulation( + size=(1, 1, 1), + grid_size=(0.1, 0.1, 0.1), + medium=medium_bg, + structures=[box_transparent, box], + sources=[src], + ) """ geometry """ @@ -411,7 +434,7 @@ def test_modes(): """ names """ -def test_names_default(): +def _test_names_default(): """makes sure default names are set""" sim = Simulation( diff --git a/tests/test_grid.py b/tests/test_grid.py index 4165d757c0..1b5a6e7352 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -20,6 +20,7 @@ def test_field_grid(): c = Coords(x=x, y=y, z=z) f = FieldGrid(x=c, y=c, z=c) + def test_grid(): boundaries_x = np.arange(-1, 2, 1) @@ -48,9 +49,9 @@ def test_sim_nonuniform_small(): grid_size_x = [2, 1, 3] sim = td.Simulation( center=(1, 0, 0), - size=(size_x, 4, 4), + size=(size_x, 4, 4), grid_size=(grid_size_x, 1, 1), - pml_layers=[td.PML(num_layers=num_layers_pml_x), None, None] + pml_layers=[td.PML(num_layers=num_layers_pml_x), None, None], ) bound_coords = sim.grid.boundaries.x @@ -61,13 +62,16 @@ def test_sim_nonuniform_small(): # checks the bounds were adjusted correctly # (smaller than sim size as is, but larger than sim size with one dl added on each edge) - assert np.sum(dls) <= size_x + num_layers_pml_x*dl_min + num_layers_pml_x*dl_max - assert np.sum(dls)+dl_min+dl_max >= size_x + num_layers_pml_x*dl_min + num_layers_pml_x*dl_max + assert np.sum(dls) <= size_x + num_layers_pml_x * dl_min + num_layers_pml_x * dl_max + assert ( + np.sum(dls) + dl_min + dl_max + >= size_x + num_layers_pml_x * dl_min + num_layers_pml_x * dl_max + ) # tests that PMLs were added correctly for i in range(num_layers_pml_x): - assert np.diff(bound_coords[i:i+2]) == dl_min - assert np.diff(bound_coords[-2-i:len(bound_coords)-i]) == dl_max + assert np.diff(bound_coords[i : i + 2]) == dl_min + assert np.diff(bound_coords[-2 - i : len(bound_coords) - i]) == dl_max # tests that all the grid sizes are in there for size in grid_size_x: @@ -78,7 +82,8 @@ def test_sim_nonuniform_small(): assert dl in grid_size_x # tests that it gives exactly what we expect - assert np.all(bound_coords == np.array([-12,-10,-8,-6,-4,-2,0,1,4,7,10,13,16])) + assert np.all(bound_coords == np.array([-12, -10, -8, -6, -4, -2, 0, 1, 4, 7, 10, 13, 16])) + def test_sim_nonuniform_large(): # tests when the nonuniform grid extends beyond the simulation size @@ -88,9 +93,9 @@ def test_sim_nonuniform_large(): grid_size_x = [2, 3, 4, 1, 2, 1, 3, 1, 2, 3, 4] sim = td.Simulation( center=(1, 0, 0), - size=(size_x, 4, 4), + size=(size_x, 4, 4), grid_size=(grid_size_x, 1, 1), - pml_layers=[td.PML(num_layers=num_layers_pml_x), None, None] + pml_layers=[td.PML(num_layers=num_layers_pml_x), None, None], ) bound_coords = sim.grid.boundaries.x @@ -101,13 +106,16 @@ def test_sim_nonuniform_large(): # checks the bounds were adjusted correctly # (smaller than sim size as is, but larger than sim size with one dl added on each edge) - assert np.sum(dls) <= size_x + num_layers_pml_x*dl_min + num_layers_pml_x*dl_max - assert np.sum(dls)+dl_min+dl_max >= size_x + num_layers_pml_x*dl_min + num_layers_pml_x*dl_max + assert np.sum(dls) <= size_x + num_layers_pml_x * dl_min + num_layers_pml_x * dl_max + assert ( + np.sum(dls) + dl_min + dl_max + >= size_x + num_layers_pml_x * dl_min + num_layers_pml_x * dl_max + ) # tests that PMLs were added correctly for i in range(num_layers_pml_x): - assert np.diff(bound_coords[i:i+2]) == grid_size_x[0] - assert np.diff(bound_coords[-2-i:len(bound_coords)-i]) == grid_size_x[-1] + assert np.diff(bound_coords[i : i + 2]) == grid_size_x[0] + assert np.diff(bound_coords[-2 - i : len(bound_coords) - i]) == grid_size_x[-1] # tests that all the grid sizes are in there for size in grid_size_x: diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 28be59b8bd..7571af0bb2 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -15,12 +15,12 @@ from .grid import Coords1D, Grid, Coords from .medium import Medium, MediumType, AbstractMedium from .structure import Structure -from .source import SourceType +from .source import SourceType, PlaneWave from .monitor import MonitorType from .pml import PMLTypes, PML from .viz import StructMediumParams, StructEpsParams, PMLParams, SymParams, add_ax_if_none from ..constants import inf, C_0 -from ..log import log, Tidy3dKeyError +from ..log import log, Tidy3dKeyError, SetupError # for docstring examples from .geometry import Sphere, Cylinder, PolySlab # pylint:disable=unused-import @@ -209,15 +209,17 @@ def _warn_sources_mediums_frequency_range(cls, val, values): # make sure medium frequency range includes the source frequency range fmin_med, fmax_med = medium.frequency_range if fmin_med > fmin_src or fmax_med < fmax_src: - log.warning(f"A medium in the simulation:\n\n({medium})\n\nhas a frequency " - "range that does not fully cover the spetrum of a source:" - f"\n\n({source})\n\nThis can cause innacuracies in the " - "simulation results.") + log.warning( + f"A medium in the simulation:\n\n({medium})\n\nhas a frequency " + "range that does not fully cover the spetrum of a source:" + f"\n\n({source})\n\nThis can cause innacuracies in the " + "simulation results." + ) return val @pydantic.validator("sources", always=True) def _warn_grid_size_too_small(cls, val, values): - """Warn user if any sources have frequency range outside of medium frequency range.""" + """Warn user if any grid size is too large compared to minimum wavelength in material.""" structures = values.get("structures") medium_bg = values.get("medium") grid_size = values.get("grid_size") @@ -236,16 +238,50 @@ def _warn_grid_size_too_small(cls, val, values): for dl in grid_size: if isinstance(dl, float): if dl > lambda_min / MIN_GRIDS_PER_WVL: - log.warning(f"One of the grid sizes with a value of {dl:.4f} (um) " - "was detected as being too large when compared to the " - "central wavelength of a source within one of the " - f"simulation mediums {lambda_min:.4f} (um). " - "To avoid inaccuracies, it is reccomended the grid size is " - "reduced." - ) + log.warning( + f"One of the grid sizes with a value of {dl:.4f} (um) " + "was detected as being too large when compared to the " + "central wavelength of a source within one of the " + f"simulation mediums {lambda_min:.4f} (um). " + "To avoid inaccuracies, it is reccomended the grid size is " + "reduced." + ) return val + @pydantic.validator("sources", always=True) + def _plane_wave_homogeneous(cls, val, values): + """Error if plane wave intersects""" + + # list of structures including background as a Box() + structure_bg = Structure( + geometry=Box( + size=values.get("size"), + center=values.get("center"), + ), + medium = values.get("medium") + ) + total_structures = [structure_bg] + values.get("structures") + + # for each plane wave in the sources list + for source in val: + if isinstance(source, PlaneWave): + + # get all merged structures on the plane + normal_axis_index = source.size.index(0.0) + dim = 'xyz'[normal_axis_index] + pos = source.center[normal_axis_index] + xyz_kwargs = {dim: pos} + structures_merged = cls._filter_structures_plane(total_structures, **xyz_kwargs) + + # make sure there is no more than one medium in the returned list + mediums = {medium for medium, _ in structures_merged} + if len(mediums) > 1: + raise SetupError(f"{len(mediums)} different mediums detected on plane " + "intersecting a plane wave source. Plane must be homogeneous." + ) + + return val """ Accounting """ @@ -396,7 +432,7 @@ def plot_structures( """ medium_map = self.medium_map - medium_shapes = self._filter_plot_structures(x=x, y=y, z=z) + medium_shapes = self._filter_structures_plane(x=x, y=y, z=z) for (medium, shape) in medium_shapes: params_updater = StructMediumParams(medium=medium, medium_map=medium_map) kwargs_struct = params_updater.update_params(**kwargs) @@ -463,7 +499,7 @@ def plot_structures_eps( # pylint: disable=too-many-arguments,too-many-locals eps_list.append(self.medium.eps_model(freq).real) eps_max = max(eps_list) eps_min = min(eps_list) - medium_shapes = self._filter_plot_structures(x=x, y=y, z=z) + medium_shapes = self._filter_structures_plane(x=x, y=y, z=z) for (medium, shape) in medium_shapes: eps = medium.eps_model(freq).real params_updater = StructEpsParams(eps=eps, eps_max=eps_max) @@ -726,8 +762,9 @@ def _set_plot_bounds(self, ax: Ax, x: float = None, y: float = None, z: float = ax.set_ylim(ymin - pml_thick_y[0], ymax + pml_thick_y[1]) return ax - def _filter_plot_structures( - self, x: float = None, y: float = None, z: float = None + @staticmethod + def _filter_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}. Overlaps are removed or merged depending on medium. @@ -748,40 +785,18 @@ def _filter_plot_structures( """ shapes = [] - for struct in self.structures: + for structure in structures: # dont bother with geometries that dont intersect plane - if not struct.geometry.intersects_plane(x=x, y=y, z=z): + if not structure.geometry.intersects_plane(x=x, y=y, z=z): continue # get list of Shapely shapes that intersect at the plane - shapes_plane = struct.geometry.intersections(x=x, y=y, z=z) + shapes_plane = structure.geometry.intersections(x=x, y=y, z=z) # Append each of them and their medium information to the list of shapes for shape in shapes_plane: - shapes.append((struct.medium, shape)) - - # returned a merged list of mediums and shapes. - return self._merge_shapes(shapes) - - @staticmethod - def _merge_shapes(shapes: List[Tuple[Medium, Shapely]]) -> List[Tuple[Medium, Shapely]]: - # pylint:disable=line-too-long - """Merge list of shapes and mediums on plae by intersection with same medium. - Edit background shapes to remove volume by intersection. - - Parameters - ---------- - shapes : List[Tuple[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`, shapely.geometry.base.BaseGeometry]] - Ordered list of shapes and their mediums on a plane. - - Returns - ------- - List[Tuple[:AbstractMedium`, shapely.geometry.base.BaseGeometry]] - Shapes and their mediums on a plane - after merging and removing intersections with background. - """ - # pylint:enable=line-too-long + shapes.append((structure.medium, shape)) background_shapes = [] for medium, shape in shapes: @@ -877,11 +892,11 @@ def _make_bound_coords_nonuniform(dl, center, size, num_layers): bound_coords = np.array([np.sum(dl[:i]) for i in range(len(dl) + 1)]) # shift coords to center at center of simulation along dimension - bound_coords = bound_coords - np.sum(dl)/2 + center + bound_coords = bound_coords - np.sum(dl) / 2 + center # chop off any coords outside of simulation bounds - bound_min = center - size/2 - bound_max = center + size/2 + bound_min = center - size / 2 + bound_max = center + size / 2 bound_coords = bound_coords[bound_coords <= bound_max] bound_coords = bound_coords[bound_coords >= bound_min] From e3d710c6149105065615ca6ce62479f30a9c1520 Mon Sep 17 00:00:00 2001 From: tylerflex Date: Tue, 14 Dec 2021 14:18:11 -0800 Subject: [PATCH 3/5] added structure extent checking and num medium checking --- tests/test_components.py | 57 ++++++++++++++------------ tests/utils.py | 24 +++++++++++ tidy3d/components/simulation.py | 72 ++++++++++++++++++++++++++++++--- 3 files changed, 122 insertions(+), 31 deletions(-) diff --git a/tests/test_components.py b/tests/test_components.py index ada17fa101..fc7dfcb54c 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -5,6 +5,7 @@ from tidy3d import * from tidy3d.log import ValidationError, SetupError +from .utils import assert_log_level def test_sim(): @@ -119,30 +120,6 @@ def test_sim_grid_size(): _ = Simulation(size=size, grid_size=(1.0, 1.0, 1.0)) -def assert_log_level(caplog, log_level_expected): - """ensure something got logged if log_level is not None""" - - # get log output - logs = caplog.record_tuples - - # there's a log but the log level is not None (problem) - if logs and not log_level_expected: - raise Exception - - # we expect a log but none is given (problem) - if log_level_expected and not logs: - raise Exception - - # both expected and got log, check the log levels match - if logs and log_level_expected: - for log in logs: - log_level = log[1] - if log_level == log_level_expected: - # log level was triggered, exit - return - raise Exception - - @pytest.mark.parametrize("fwidth,log_level", [(0.001, None), (3, 30)]) def test_sim_frequency_range(caplog, fwidth, log_level): # small fwidth should be inside range, large one should throw warning @@ -177,7 +154,7 @@ def test_sim_grid_size(caplog, grid_size, log_level): def test_sim_plane_wave_error(): - # small fwidth should be inside range, large one should throw warning + """ "Make sure we error if plane wave is not intersecting homogeneous region of simulation.""" medium_bg = Medium(permittivity=2) medium_air = Medium(permittivity=1) @@ -213,6 +190,35 @@ def test_sim_plane_wave_error(): sources=[src], ) + +@pytest.mark.parametrize( + "box_size,log_level", + [((0.1, 0.1, 0.1), None), ((1, 0.1, 0.1), 30), ((0.1, 1, 0.1), 30), ((0.1, 0.1, 1), 30)], +) +def test_sim_structure_extent(caplog, box_size, log_level): + """Make sure we warn if structure extends exactly to simulation edges.""" + + box = Structure(geometry=Box(size=box_size), medium=Medium(permittivity=2)) + sim = Simulation(size=(1, 1, 1), grid_size=(0.1, 0.1, 0.1), structures=[box]) + + assert_log_level(caplog, log_level) + + +def test_num_mediums(): + """Make sure we error if too many mediums supplied.""" + + structures = [] + for i in range(200): + structures.append(Structure(geometry=Box(size=(1, 1, 1)), medium=Medium(permittivity=i+1))) + sim = Simulation(size=(1, 1, 1), grid_size=(0.1, 0.1, 0.1), structures=structures) + + with pytest.raises(SetupError): + structures.append( + Structure(geometry=Box(size=(1, 1, 1)), medium=Medium(permittivity=i+2)) + ) + sim = Simulation(size=(1, 1, 1), grid_size=(0.1, 0.1, 0.1), structures=structures) + + """ geometry """ @@ -353,7 +359,6 @@ def test_medium_dispersion_create(): def eps_compare(medium: Medium, expected: Dict, tol: float = 1e-5): for freq, val in expected.items(): - # print(f"Expected: {medium.eps_model(freq)}, got: {val}") assert np.abs(medium.eps_model(freq) - val) < tol diff --git a/tests/utils.py b/tests/utils.py index 8604534363..91fb96c394 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -33,6 +33,30 @@ def prepend_tmp(path): return os.path.join(TMP_DIR, path) +def assert_log_level(caplog, log_level_expected): + """ensure something got logged if log_level is not None""" + + # get log output + logs = caplog.record_tuples + + # there's a log but the log level is not None (problem) + if logs and not log_level_expected: + raise Exception + + # we expect a log but none is given (problem) + if log_level_expected and not logs: + raise Exception + + # both expected and got log, check the log levels match + if logs and log_level_expected: + for log in logs: + log_level = log[1] + if log_level == log_level_expected: + # log level was triggered, exit + return + raise Exception + + SIM_MONITORS = Simulation( size=(2.0, 2.0, 2.0), grid_size=(0.1, 0.1, 0.1), diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 7571af0bb2..801039d7e4 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -33,6 +33,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 = 200 + class Simulation(Box): # pylint:disable=too-many-public-methods # pylint:disable=line-too-long @@ -191,10 +194,58 @@ def set_none_to_zero_layers(cls, val): _unique_monitor_names = assert_unique_names("monitors") # _unique_medium_names = assert_unique_names("structures", check_mediums=True) + @pydantic.validator("structures", always=True) + def _validate_num_mediums(cls, val): + """Error if too many mediums present.""" + + if not val: + return + + 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("structures", always=True) + def _structures_not_at_edges(cls, val, values): + """Warn if any structures lie at the simulation boundaries.""" + + if not val: + return + + 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) + + for structure in 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 np.isclose(sim_val, struct_val): + log.warning( + f"structure\n\n{structure}\n\nbounds 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 + @pydantic.validator("sources", always=True) def _warn_sources_mediums_frequency_range(cls, val, values): """Warn user if any sources have frequency range outside of medium frequency range.""" + + if not val: + return + structures = values.get("structures") + structures = [] if not structures else structures medium_bg = values.get("medium") mediums = [medium_bg] + [structure.medium for structure in structures] @@ -220,7 +271,12 @@ def _warn_sources_mediums_frequency_range(cls, val, values): @pydantic.validator("sources", always=True) def _warn_grid_size_too_small(cls, val, values): """Warn user if any grid size is too large compared to minimum wavelength in material.""" + + if not val: + return + structures = values.get("structures") + structures = [] if not structures else structures medium_bg = values.get("medium") grid_size = values.get("grid_size") mediums = [medium_bg] + [structure.medium for structure in structures] @@ -253,15 +309,21 @@ def _warn_grid_size_too_small(cls, val, values): def _plane_wave_homogeneous(cls, val, values): """Error if plane wave intersects""" + if not val: + return + # list of structures including background as a Box() structure_bg = Structure( geometry=Box( size=values.get("size"), center=values.get("center"), ), - medium = values.get("medium") + medium=values.get("medium"), ) - total_structures = [structure_bg] + values.get("structures") + + structures = values.get("structures") + structures = [] if not structures else structures + total_structures = [structure_bg] + structures # for each plane wave in the sources list for source in val: @@ -269,7 +331,7 @@ def _plane_wave_homogeneous(cls, val, values): # get all merged structures on the plane normal_axis_index = source.size.index(0.0) - dim = 'xyz'[normal_axis_index] + dim = "xyz"[normal_axis_index] pos = source.center[normal_axis_index] xyz_kwargs = {dim: pos} structures_merged = cls._filter_structures_plane(total_structures, **xyz_kwargs) @@ -277,13 +339,13 @@ def _plane_wave_homogeneous(cls, val, values): # make sure there is no more than one medium in the returned list mediums = {medium for medium, _ in structures_merged} if len(mediums) > 1: - raise SetupError(f"{len(mediums)} different mediums detected on plane " + raise SetupError( + f"{len(mediums)} different mediums detected on plane " "intersecting a plane wave source. Plane must be homogeneous." ) return val - """ Accounting """ @property From a3d3cd1d3e7ade90ca4b129ce604b0267583639b Mon Sep 17 00:00:00 2001 From: tylerflex Date: Tue, 14 Dec 2021 15:21:56 -0800 Subject: [PATCH 4/5] added all validators, gap size pml --- tests/test_IO.py | 10 ++++- tests/test_components.py | 54 ++++++++++++++++++++-- tests/utils.py | 28 +----------- tidy3d/components/simulation.py | 80 +++++++++++++++++++++++++++------ tidy3d/components/validators.py | 10 ++--- 5 files changed, 132 insertions(+), 50 deletions(-) diff --git a/tests/test_IO.py b/tests/test_IO.py index d880bedca6..c46baada4e 100644 --- a/tests/test_IO.py +++ b/tests/test_IO.py @@ -23,7 +23,7 @@ def test_simulation_preserve_types(): st = GaussianPulse(freq0=1.0, fwidth=1.0) sim_all = Simulation( - size=(1.0, 1.0, 1.0), + size=(10.0, 10.0, 10.0), grid_size=(1, 1, 1), structures=[ Structure(geometry=Box(size=(1, 1, 1)), medium=Medium()), @@ -40,7 +40,13 @@ def test_simulation_preserve_types(): ], sources=[ VolumeSource(size=(0, 0, 0), source_time=st, polarization="Ex"), - PlaneWave(size=(inf, inf, 0), source_time=st, direction="+", polarization="Ex"), + PlaneWave( + size=(inf, inf, 0), + center=(0, 0, 4.5), + source_time=st, + direction="+", + polarization="Ex", + ), GaussianBeam( size=(inf, inf, 0), source_time=st, diff --git a/tests/test_components.py b/tests/test_components.py index fc7dfcb54c..449945277d 100644 --- a/tests/test_components.py +++ b/tests/test_components.py @@ -5,7 +5,33 @@ from tidy3d import * from tidy3d.log import ValidationError, SetupError -from .utils import assert_log_level + + +def assert_log_level(caplog, log_level_expected): + """ensure something got logged if log_level is not None. + note: I put this here rather than utils.py because if we import from utils.py, + it will validate the sims there and those get included in log. + """ + + # get log output + logs = caplog.record_tuples + + # there's a log but the log level is not None (problem) + if logs and not log_level_expected: + raise Exception + + # we expect a log but none is given (problem) + if log_level_expected and not logs: + raise Exception + + # both expected and got log, check the log levels match + if logs and log_level_expected: + for log in logs: + log_level = log[1] + if log_level == log_level_expected: + # log level was triggered, exit + return + raise Exception def test_sim(): @@ -153,6 +179,26 @@ def test_sim_grid_size(caplog, grid_size, log_level): assert_log_level(caplog, log_level) +@pytest.mark.parametrize("box_size,log_level", [(0.001, None), (9.9, 30), (20, None)]) +def test_sim_structure_gap(caplog, box_size, log_level): + """Make sure the gap between a structure and PML is not too small compared to lambda0.""" + medium = Medium(permittivity=2) + box = Structure(geometry=Box(size=(box_size, box_size, box_size)), medium=medium) + src = VolumeSource( + source_time=GaussianPulse(freq0=3e14, fwidth=1e13), + size=(0, 0, 0), + polarization="Ex", + ) + sim = Simulation( + size=(10, 10, 10), + grid_size=(0.1, 0.1, 0.1), + structures=[box], + sources=[src], + pml_layers=[PML(num_layers=5), PML(num_layers=5), PML(num_layers=5)], + ) + assert_log_level(caplog, log_level) + + def test_sim_plane_wave_error(): """ "Make sure we error if plane wave is not intersecting homogeneous region of simulation.""" @@ -209,12 +255,14 @@ def test_num_mediums(): structures = [] for i in range(200): - structures.append(Structure(geometry=Box(size=(1, 1, 1)), medium=Medium(permittivity=i+1))) + structures.append( + Structure(geometry=Box(size=(1, 1, 1)), medium=Medium(permittivity=i + 1)) + ) sim = Simulation(size=(1, 1, 1), grid_size=(0.1, 0.1, 0.1), structures=structures) with pytest.raises(SetupError): structures.append( - Structure(geometry=Box(size=(1, 1, 1)), medium=Medium(permittivity=i+2)) + Structure(geometry=Box(size=(1, 1, 1)), medium=Medium(permittivity=i + 2)) ) sim = Simulation(size=(1, 1, 1), grid_size=(0.1, 0.1, 0.1), structures=structures) diff --git a/tests/utils.py b/tests/utils.py index 91fb96c394..ad35a18469 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -33,32 +33,8 @@ def prepend_tmp(path): return os.path.join(TMP_DIR, path) -def assert_log_level(caplog, log_level_expected): - """ensure something got logged if log_level is not None""" - - # get log output - logs = caplog.record_tuples - - # there's a log but the log level is not None (problem) - if logs and not log_level_expected: - raise Exception - - # we expect a log but none is given (problem) - if log_level_expected and not logs: - raise Exception - - # both expected and got log, check the log levels match - if logs and log_level_expected: - for log in logs: - log_level = log[1] - if log_level == log_level_expected: - # log level was triggered, exit - return - raise Exception - - SIM_MONITORS = Simulation( - size=(2.0, 2.0, 2.0), + size=(10.0, 10.0, 10.0), grid_size=(0.1, 0.1, 0.1), monitors=[ FieldMonitor(size=(1, 1, 1), center=(0, 1, 0), freqs=[1, 2, 5, 7, 8], name="field_freq"), @@ -76,7 +52,7 @@ def assert_log_level(caplog, log_level_expected): ) SIM_FULL = Simulation( - size=(2.0, 2.0, 2.0), + size=(10.0, 10.0, 10.0), grid_size=(0.1, 0.1, 0.1), run_time=40e-11, structures=[ diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 801039d7e4..f9111bd11b 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -194,12 +194,19 @@ def set_none_to_zero_layers(cls, val): _unique_monitor_names = assert_unique_names("monitors") # _unique_medium_names = assert_unique_names("structures", check_mediums=True) + # _few_enough_mediums = validate_num_mediums() + # _structures_not_at_edges = validate_structure_bounds_not_at_edges() + # _gap_size_ok = validate_pml_gap_size() + # _medium_freq_range_ok = validate_medium_frequency_range() + # _resolution_fine_enough = validate_resolution() + # _plane_waves_in_homo = validate_plane_wave_intersections() + @pydantic.validator("structures", always=True) def _validate_num_mediums(cls, val): """Error if too many mediums present.""" - if not val: - return + if val is None: + return val mediums = {structure.medium for structure in val} if len(mediums) > MAX_NUM_MEDIUMS: @@ -214,8 +221,8 @@ def _validate_num_mediums(cls, val): def _structures_not_at_edges(cls, val, values): """Warn if any structures lie at the simulation boundaries.""" - if not val: - return + 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 @@ -237,12 +244,57 @@ def _structures_not_at_edges(cls, val, values): return val + @pydantic.validator("pml_layers", always=True) + def _structures_not_close_pml(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 + + structures = values.get("structures") + sources = values.get("sources") + if (not structures) or (not sources): + return val + + for structure in structures: + struct_bound_min, struct_bound_max = structure.geometry.bounds + + for source in sources: + fmin_src, fmax_src = source.source_time.frequency_range + f_average = (fmin_src + fmax_src) / 2.0 + lambda0 = C_0 / f_average + + for sim_val, struct_val, pml in zip(sim_bound_min, struct_bound_min, val): + if pml.num_layers > 0 and struct_val > sim_val: + if abs(sim_val - struct_val) < lambda0 / 2: + log.warning( + f"a structure\n\n{structure}\n\nwas detected as being less " + "than half of a central wavelength from a PML on - side" + "To avoid inaccurate results, please increase gap between " + "any structures and PML." + ) + + for sim_val, struct_val, pml in zip(sim_bound_max, struct_bound_max, val): + if pml.num_layers > 0 and struct_val < sim_val: + if abs(sim_val - struct_val) < lambda0 / 2: + log.warning( + f"a structure\n\n{structure}\n\nwas detected as being less " + "than half of a central wavelength from a PML on + side" + "To avoid inaccurate results, please increase gap between " + "any structures and PML." + ) + + return val + @pydantic.validator("sources", always=True) def _warn_sources_mediums_frequency_range(cls, val, values): """Warn user if any sources have frequency range outside of medium frequency range.""" - if not val: - return + if val is None: + return val structures = values.get("structures") structures = [] if not structures else structures @@ -272,11 +324,11 @@ def _warn_sources_mediums_frequency_range(cls, val, values): def _warn_grid_size_too_small(cls, val, values): """Warn user if any grid size is too large compared to minimum wavelength in material.""" - if not val: - return + if val is None: + return val structures = values.get("structures") - structures = [] if not structures else structures + structures = [] if not structures else structures medium_bg = values.get("medium") grid_size = values.get("grid_size") mediums = [medium_bg] + [structure.medium for structure in structures] @@ -309,8 +361,8 @@ def _warn_grid_size_too_small(cls, val, values): def _plane_wave_homogeneous(cls, val, values): """Error if plane wave intersects""" - if not val: - return + if val is None: + return val # list of structures including background as a Box() structure_bg = Structure( @@ -322,7 +374,7 @@ def _plane_wave_homogeneous(cls, val, values): ) structures = values.get("structures") - structures = [] if not structures else structures + structures = [] if not structures else structures total_structures = [structure_bg] + structures # for each plane wave in the sources list @@ -494,7 +546,7 @@ def plot_structures( """ medium_map = self.medium_map - medium_shapes = self._filter_structures_plane(x=x, y=y, z=z) + medium_shapes = self._filter_structures_plane(self.structures, x=x, y=y, z=z) for (medium, shape) in medium_shapes: params_updater = StructMediumParams(medium=medium, medium_map=medium_map) kwargs_struct = params_updater.update_params(**kwargs) @@ -561,7 +613,7 @@ def plot_structures_eps( # pylint: disable=too-many-arguments,too-many-locals eps_list.append(self.medium.eps_model(freq).real) eps_max = max(eps_list) eps_min = min(eps_list) - medium_shapes = self._filter_structures_plane(x=x, y=y, z=z) + medium_shapes = self._filter_structures_plane(self.structures, x=x, y=y, z=z) for (medium, shape) in medium_shapes: eps = medium.eps_model(freq).real params_updater = StructEpsParams(eps=eps, eps_max=eps_max) diff --git a/tidy3d/components/validators.py b/tidy3d/components/validators.py index 428b8a50d4..8657dc1d42 100644 --- a/tidy3d/components/validators.py +++ b/tidy3d/components/validators.py @@ -40,7 +40,7 @@ def assert_plane(): - """Makes sure a field's `size` attribute has exactly 1 zero.""" + """makes sure a field's `size` attribute has exactly 1 zero""" @pydantic.validator("size", allow_reuse=True, always=True) def is_plane(cls, val): @@ -52,7 +52,7 @@ def is_plane(cls, val): def validate_name_str(): - """Make sure the name doesnt include [, ] (used for default names).""" + """make sure the name doesnt include [, ] (used for default names)""" @pydantic.validator("name", allow_reuse=True, always=True, pre=True) def field_has_unique_names(cls, val): @@ -65,7 +65,7 @@ def field_has_unique_names(cls, val): def assert_unique_names(field_name: str, check_mediums=False): - """Makes sure all elements of a field have unique .name values.""" + """makes sure all elements of a field have unique .name values""" @pydantic.validator(field_name, allow_reuse=True, always=True) def field_has_unique_names(cls, val, values): @@ -85,7 +85,7 @@ def field_has_unique_names(cls, val, values): def assert_objects_in_sim_bounds(field_name: str): - """Makes sure all objects in field are at least partially inside of simulation bounds.""" + """makes sure all objects in field are at least partially inside of simulation bounds/""" @pydantic.validator(field_name, allow_reuse=True, always=True) def objects_in_sim_bounds(cls, val, values): @@ -104,7 +104,7 @@ def objects_in_sim_bounds(cls, val, values): def set_names(field_name: str): - """Set names.""" + """set names""" @pydantic.validator(field_name, allow_reuse=True, always=True) def set_unique_names(cls, val): From c9b1cd8b51c6d4acf5739a491c3d82123c10fc8f Mon Sep 17 00:00:00 2001 From: tylerflex Date: Tue, 28 Dec 2021 12:00:50 -0800 Subject: [PATCH 5/5] incorporated momchils comments; --- tidy3d/components/simulation.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index f9111bd11b..6b092cfc6e 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -1,3 +1,4 @@ +# pylint:disable=too-many-lines """ Container holding all information about simulation and its components""" from typing import Dict, Tuple, List, Set @@ -9,7 +10,7 @@ from mpl_toolkits.axes_grid1 import make_axes_locatable from descartes import PolygonPatch -from .validators import assert_unique_names, assert_objects_in_sim_bounds, set_names +from .validators import assert_unique_names, assert_objects_in_sim_bounds from .geometry import Box from .types import Symmetry, Ax, Shapely, FreqBound, GridSize from .grid import Coords1D, Grid, Coords @@ -27,8 +28,6 @@ from .source import VolumeSource, GaussianPulse # pylint:disable=unused-import from .monitor import FieldMonitor, FluxMonitor, Monitor # pylint:disable=unused-import -# technically this is creating a circular import issue because it calls tidy3d/__init__.py -# from .. import __version__ as version_number # minimum number of grid points allowed per central wavelength in a medium MIN_GRIDS_PER_WVL = 6.0 @@ -245,7 +244,7 @@ def _structures_not_at_edges(cls, val, values): return val @pydantic.validator("pml_layers", always=True) - def _structures_not_close_pml(cls, val, values): + def _structures_not_close_pml(cls, val, values): # pylint:disable=too-many-locals """Warn if any structures lie at the simulation boundaries.""" if val is None: @@ -259,6 +258,15 @@ def _structures_not_close_pml(cls, val, values): if (not structures) or (not sources): return val + def warn(structure): + """Warning message for a structure too close to PML.""" + log.warning( + f"a structure\n\n{structure}\n\nwas detected as being less " + "than half of a central wavelength from a PML on - side" + "To avoid inaccurate results, please increase gap between " + "any structures and PML or fully extend structure through the pml." + ) + for structure in structures: struct_bound_min, struct_bound_max = structure.geometry.bounds @@ -270,22 +278,12 @@ def _structures_not_close_pml(cls, val, values): for sim_val, struct_val, pml in zip(sim_bound_min, struct_bound_min, val): if pml.num_layers > 0 and struct_val > sim_val: if abs(sim_val - struct_val) < lambda0 / 2: - log.warning( - f"a structure\n\n{structure}\n\nwas detected as being less " - "than half of a central wavelength from a PML on - side" - "To avoid inaccurate results, please increase gap between " - "any structures and PML." - ) + warn(structure) for sim_val, struct_val, pml in zip(sim_bound_max, struct_bound_max, val): if pml.num_layers > 0 and struct_val < sim_val: if abs(sim_val - struct_val) < lambda0 / 2: - log.warning( - f"a structure\n\n{structure}\n\nwas detected as being less " - "than half of a central wavelength from a PML on + side" - "To avoid inaccurate results, please increase gap between " - "any structures and PML." - ) + warn(structure) return val @@ -321,7 +319,7 @@ def _warn_sources_mediums_frequency_range(cls, val, values): return val @pydantic.validator("sources", always=True) - def _warn_grid_size_too_small(cls, val, values): + def _warn_grid_size_too_small(cls, val, values): # pylint:disable=too-many-locals """Warn user if any grid size is too large compared to minimum wavelength in material.""" if val is None: @@ -354,6 +352,7 @@ def _warn_grid_size_too_small(cls, val, values): "To avoid inaccuracies, it is reccomended the grid size is " "reduced." ) + # TODO: nonuniform grid return val