diff --git a/docs/api.rst b/docs/api.rst index 7b82ce8b33..883328767d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -27,6 +27,7 @@ Methods Simulation.plot_monitors Simulation.plot_symmetries Simulation.plot_pml + Simulation.plot_grid Simulation.grid Simulation.dt Simulation.tmesh @@ -84,6 +85,7 @@ Geometry Sphere Cylinder PolySlab + PolySlab.from_gdspy Methods ------- @@ -259,7 +261,9 @@ Log .. autosummary:: :toctree: _autosummary/ - logging_level + log + set_logging_level + set_logging_file Submitting Simulations diff --git a/test_static.sh b/test_static.sh index 0c82c78d0c..c7b7eb5cac 100755 --- a/test_static.sh +++ b/test_static.sh @@ -10,6 +10,7 @@ pytest -rA tests/test_plugins.py pytest --doctest-modules tidy3d \ --ignore=tidy3d/__main__.py \ +--ignore=tidy3d/log.py \ --ignore=tidy3d/components/base.py \ --ignore=tidy3d/web/webapi.py \ --ignore=tidy3d/web/container.py \ diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 1107dcae77..123b5da297 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -53,7 +53,7 @@ from .material_library import material_library # logging -from .log import log, logging_level +from .log import log, set_logging_level, set_logging_file # for docs from .components.medium import AbstractMedium diff --git a/tidy3d/components/data.py b/tidy3d/components/data.py index 8b21caf182..24951db744 100644 --- a/tidy3d/components/data.py +++ b/tidy3d/components/data.py @@ -106,6 +106,7 @@ class MonitorData(Tidy3dData, ABC): @property def data(self) -> xr.DataArray: + # pylint:disable=line-too-long """Returns an xarray representation of the montitor data. Returns @@ -114,6 +115,7 @@ def data(self) -> xr.DataArray: Representation of the monitor data using xarray. For more details refer to `xarray's Documentaton `_. """ + # pylint:enable=line-too-long data_dict = self.dict() coords = {dim: data_dict[dim] for dim in self._dims} @@ -187,6 +189,7 @@ class CollectionData(Tidy3dData): @property def data(self) -> xr.Dataset: + # pylint:disable=line-too-long """For field quantities, store a single xarray DataArray for each ``field``. These all go in a single xarray Dataset, which keeps track of the shared coords. @@ -196,6 +199,7 @@ def data(self) -> xr.Dataset: Representation of the underlying data using xarray. For more details refer to `xarray's Documentaton `_. """ + # pylint:enable=line-too-long data_arrays = {name: arr.data for name, arr in self.data_dict.items()} # make an xarray dataset diff --git a/tidy3d/components/geometry.py b/tidy3d/components/geometry.py index 9e3936349e..17e2ba69f0 100644 --- a/tidy3d/components/geometry.py +++ b/tidy3d/components/geometry.py @@ -1,3 +1,4 @@ +# pylint:disable=too-many-lines """Defines spatial extent of objects.""" from abc import ABC, abstractmethod @@ -65,7 +66,8 @@ def intersections(self, x: float = None, y: float = None, z: float = None) -> Li ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ def intersects(self, other) -> bool: @@ -180,6 +182,7 @@ def _pop_bounds(self, axis: Axis) -> Tuple[Coordinate2D, Tuple[Coordinate2D, Coo def plot( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **patch_kwargs ) -> Ax: + # pylint:disable=line-too-long """Plot geometry cross section at single (x,y,z) coordinate. Parameters @@ -202,6 +205,7 @@ def plot( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:disable=line-too-long # find shapes that intersect self at plane axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) @@ -373,7 +377,8 @@ def intersections(self, x: float = None, y: float = None, z: float = None): ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) if axis == self.axis: @@ -391,7 +396,8 @@ def _intersections_normal(self) -> list: ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ @abstractmethod @@ -409,7 +415,8 @@ def _intersections_side(self, position: float, axis: Axis) -> list: ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ @property @@ -519,7 +526,8 @@ def intersections(self, x: float = None, y: float = None, z: float = None): ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) z0, (x0, y0) = self.pop_axis(self.center, axis=axis) @@ -636,7 +644,8 @@ def intersections(self, x: float = None, y: float = None, z: float = None): ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ axis, position = self._parse_xyz_kwargs(x=x, y=y, z=z) z0, (x0, y0) = self.pop_axis(self.center, axis=axis) @@ -688,7 +697,8 @@ def _intersections_normal(self): ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ _, (x0, y0) = self.pop_axis(self.center, axis=self.axis) return [Point(x0, y0).buffer(self.radius)] @@ -707,7 +717,8 @@ def _intersections_side(self, position, axis): ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ z0_axis, _ = self.pop_axis(self.center, axis=self.axis) intersect_dist = self._intersect_dist(position, z0_axis) @@ -912,7 +923,8 @@ def _intersections_normal(self): ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ return [Polygon(self.vertices)] @@ -930,7 +942,8 @@ def _intersections_side(self, position, axis) -> list: # pylint:disable=too-man ------- List[shapely.geometry.base.BaseGeometry] List of 2D shapes that intersect plane. - For more details refer to `Shapely's Documentaton `_. + For more details refer to + `Shapely's Documentaton `_. """ z0, _ = self.pop_axis(self.center, axis=self.axis) diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index d60ee1e06c..4f347ef6eb 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -20,7 +20,7 @@ class Monitor(Box, ABC): _name_validator = validate_name_str() @add_ax_if_none - def plot( #pylint:disable=duplicate-code + def plot( # pylint:disable=duplicate-code self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: """Plot the monitor geometry on a cross section plane. diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index d44793e5d1..980ce7c2e7 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -32,6 +32,7 @@ class Simulation(Box): # pylint:disable=too-many-public-methods + # pylint:disable=line-too-long """Contains all information about Tidy3d simulation. Parameters @@ -48,7 +49,7 @@ class Simulation(Box): # pylint:disable=too-many-public-methods Total electromagnetic evolution time in seconds. Note: If ``shutoff`` specified, simulation will terminate early when shutoff condition met. Must be non-negative. - medium : :class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye` = ``Medium(permittivity=1.0)`` + medium : :class:`Medium` or :class:`AnisotropicMedium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye` = ``Medium(permittivity=1.0)`` Background :class:`tidy3d.Medium` of simulation, defaults to air. structures : List[:class:`Structure`] = [] List of structures in simulation. @@ -59,8 +60,7 @@ class Simulation(Box): # pylint:disable=too-many-public-methods monitors : List[:class:`FieldMonitor` or :class:`FieldTimeMonitor` or :class:`FluxMonitor` or :class:`FluxTimeMonitor` or :class:`ModeMonitor`] = [] List of monitors in the simulation. Note: names stored in ``monitor.name`` are used to access data after simulation is run. - pml_layers : Tuple[:class:`AbsorberSpec`, :class:`AbsorberSpec`, :class:`AbsorberSpec`] - = ``(None, None, None)`` + pml_layers : Tuple[:class:`AbsorberSpec`, :class:`AbsorberSpec`, :class:`AbsorberSpec`] = ``(None, None, None)`` Specifications for the absorbing layers on x, y, and z edges. Elements of ``None`` are assumed to have no absorber and use periodic boundary conditions. symmetry : Tuple[int, int, int] = (0, 0, 0) @@ -138,6 +138,7 @@ class Simulation(Box): # pylint:disable=too-many-public-methods ... subpixel=False, ... ) """ + # pylint:enable=line-too-long grid_size: Tuple[pydantic.PositiveFloat, pydantic.PositiveFloat, pydantic.PositiveFloat] medium: MediumType = Medium() @@ -196,6 +197,7 @@ def set_medium_names(cls, val, values): @property def mediums(self) -> List[MediumType]: + # pylint:disable=line-too-long """Returns set of distinct :class:`AbstractMedium` in simulation. Returns @@ -204,9 +206,11 @@ def mediums(self) -> List[MediumType]: Set of distinct mediums in the simulation. """ return {structure.medium for structure in self.structures} + # pylint:enable=line-too-long @property def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: + # pylint:disable=line-too-long """Returns dict mapping medium to index in material. ``medium_map[medium]`` returns unique global index of :class:`AbstractMedium` in simulation. @@ -215,6 +219,7 @@ def medium_map(self) -> Dict[MediumType, pydantic.NonNegativeInt]: Dict[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`, int] Mapping between distinct mediums to index in simulation. """ + # pylint:enable=line-too-long return {medium: index for index, medium in enumerate(self.mediums)} @@ -226,10 +231,10 @@ def plot( # pylint:disable=too-many-arguments x: float = None, y: float = None, z: float = None, - grid_lines: bool = False, ax: Ax = None, **kwargs, ) -> Ax: + # pylint:disable=line-too-long """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. Parameters @@ -240,8 +245,6 @@ def plot( # pylint:disable=too-many-arguments position of plane in y direction, only one of x, y, z must be specified to define plane. z : float = None position of plane in z direction, only one of x, y, z must be specified to define plane. - grid_lines : bool = False - If true, displays FDTD cell boundaries on plot. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. **kwargs @@ -254,14 +257,13 @@ def plot( # pylint:disable=too-many-arguments matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long ax = self.plot_structures(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_sources(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_pml(ax=ax, x=x, y=y, z=z, **kwargs) - if grid_lines: - ax = self.plot_cells(ax=ax, x=x, y=y, z=z) ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @@ -272,10 +274,10 @@ def plot_eps( # pylint: disable=too-many-arguments y: float = None, z: float = None, freq: float = None, - grid_lines: bool = False, ax: Ax = None, **kwargs, ) -> Ax: + # pylint:disable=line-too-long """Plot each of simulation's components on a plane defined by one nonzero x,y,z coordinate. The permittivity is plotted in grayscale based on its value at the specified frequency. @@ -290,8 +292,6 @@ def plot_eps( # pylint: disable=too-many-arguments freq : float = None Frequency to evaluate the relative permittivity of all mediums. If not specified, evaluates at infinite frequency. - grid_lines : bool = False - If true, displays FDTD cell boundaries on plot. ax : matplotlib.axes._subplots.Axes = None Matplotlib axes to plot on, if not specified, one is created. **kwargs @@ -304,14 +304,13 @@ def plot_eps( # pylint: disable=too-many-arguments matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long ax = self.plot_structures_eps(freq=freq, cbar=True, ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_sources(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_monitors(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_symmetries(ax=ax, x=x, y=y, z=z, **kwargs) ax = self.plot_pml(ax=ax, x=x, y=y, z=z, **kwargs) - if grid_lines: - ax = self.plot_cells(ax=ax, x=x, y=y, z=z) ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax @@ -319,6 +318,7 @@ def plot_eps( # pylint: disable=too-many-arguments def plot_structures( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: + # pylint:disable=line-too-long """Plot each of simulation's structures on a plane defined by one nonzero x,y,z coordinate. Parameters @@ -341,6 +341,8 @@ def plot_structures( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long + medium_map = self.medium_map medium_shapes = self._filter_plot_structures(x=x, y=y, z=z) for (medium, shape) in medium_shapes: @@ -374,6 +376,7 @@ def plot_structures_eps( # pylint: disable=too-many-arguments,too-many-locals ax: Ax = None, **kwargs, ) -> Ax: + # pylint:disable=line-too-long """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. @@ -400,6 +403,8 @@ def plot_structures_eps( # pylint: disable=too-many-arguments,too-many-locals matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long + if freq is None: freq = inf eps_list = [s.medium.eps_model(freq).real for s in self.structures] @@ -423,6 +428,7 @@ def plot_structures_eps( # pylint: disable=too-many-arguments,too-many-locals def plot_sources( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: + # pylint:disable=line-too-long """Plot each of simulation's sources on a plane defined by one nonzero x,y,z coordinate. Parameters @@ -445,6 +451,7 @@ def plot_sources( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long for source in self.sources: if source.intersects_plane(x=x, y=y, z=z): ax = source.plot(ax=ax, x=x, y=y, z=z, **kwargs) @@ -455,6 +462,7 @@ def plot_sources( def plot_monitors( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: + # pylint:disable=line-too-long """Plot each of simulation's monitors on a plane defined by one nonzero x,y,z coordinate. Parameters @@ -477,6 +485,7 @@ def plot_monitors( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long for monitor in self.monitors: if monitor.intersects_plane(x=x, y=y, z=z): ax = monitor.plot(ax=ax, x=x, y=y, z=z, **kwargs) @@ -487,6 +496,7 @@ def plot_monitors( def plot_symmetries( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: + # pylint:disable=line-too-long """Plot each of simulation's symmetries on a plane defined by one nonzero x,y,z coordinate. Parameters @@ -509,6 +519,7 @@ def plot_symmetries( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long for sym_axis, sym_value in enumerate(self.symmetry): if sym_value == 0: continue @@ -562,6 +573,7 @@ def pml_thicknesses(self) -> List[Tuple[float, float]]: def plot_pml( self, x: float = None, y: float = None, z: float = None, ax: Ax = None, **kwargs ) -> Ax: + # pylint:disable=line-too-long """Plot each of simulation's absorbing boundaries on a plane defined by one nonzero x,y,z coordinate. @@ -585,6 +597,7 @@ def plot_pml( matplotlib.axes._subplots.Axes The supplied or created matplotlib axes. """ + # pylint:enable=line-too-long kwargs = PMLParams().update_params(**kwargs) pml_thicks = self.pml_thicknesses for pml_axis, pml_layer in enumerate(self.pml_layers): @@ -603,7 +616,7 @@ def plot_pml( return ax @add_ax_if_none - def plot_cells(self, x: float = None, y: float = None, z: float = None, ax: Ax = None) -> Ax: + def plot_grid(self, x: float = None, y: float = None, z: float = None, ax: Ax = None) -> Ax: """Plot the cell boundaries as lines on a plane defined by one nonzero x,y,z coordinate. Parameters @@ -631,6 +644,7 @@ def plot_cells(self, x: float = None, y: float = None, z: float = None, ax: Ax = ax.axvline(x=x_pos, linestyle="-", color="black", linewidth=0.2) for y_pos in boundaries_y: ax.axhline(y=y_pos, linestyle="-", color="black", linewidth=0.2) + ax = self._set_plot_bounds(ax=ax, x=x, y=y, z=z) return ax def _set_plot_bounds(self, ax: Ax, x: float = None, y: float = None, z: float = None) -> Ax: @@ -664,6 +678,7 @@ def _set_plot_bounds(self, ax: Ax, x: float = None, y: float = None, z: float = def _filter_plot_structures( self, x: float = None, y: float = None, z: float = None ) -> List[Tuple[Medium, Shapely]]: + # pylint:disable=line-too-long """Compute list of shapes to plot on plane specified by {x,y,z}. Overlaps are removed or merged depending on medium. @@ -681,6 +696,7 @@ def _filter_plot_structures( List[Tuple[:class:`Medium` or :class:`PoleResidue` or :class:`Lorentz` or :class:`Sellmeier` or :class:`Debye`, shapely.geometry.base.BaseGeometry]] List of shapes and mediums on the plane after merging. """ + # pylint:enable=line-too-long shapes = [] for struct in self.structures: @@ -701,6 +717,7 @@ def _filter_plot_structures( @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. @@ -715,6 +732,8 @@ def _merge_shapes(shapes: List[Tuple[Medium, Shapely]]) -> List[Tuple[Medium, Sh Shapes and their mediums on a plane after merging and removing intersections with background. """ + # pylint:enable=line-too-long + background_shapes = [] for medium, shape in shapes: @@ -895,6 +914,7 @@ def discretize(self, box: Box) -> Grid: return Grid(boundaries=sub_boundaries) def epsilon(self, box: Box, freq: float = None) -> Dict[str, xr.DataArray]: + # pylint:disable=line-too-long """Get array of permittivity at volume specified by box and freq Parameters @@ -916,6 +936,7 @@ def epsilon(self, box: Box, freq: float = None) -> Dict[str, xr.DataArray]: at the corresponding field position in the yee lattice. For details on xarray datasets, refer to `xarray's Documentaton `_. """ + # pylint:enable=line-too-long sub_grid = self.discretize(box) eps_background = self.medium.eps_model(freq) diff --git a/tidy3d/log.py b/tidy3d/log.py index e827946b43..b77f796345 100644 --- a/tidy3d/log.py +++ b/tidy3d/log.py @@ -6,7 +6,9 @@ FORMAT = "%(message)s" -logging.basicConfig(level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]) +DEFAULT_LEVEL = "INFO" + +logging.basicConfig(level=DEFAULT_LEVEL, format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]) log = logging.getLogger("rich") @@ -59,16 +61,60 @@ class DataError(Tidy3DError): """Error accessing data.""" -def logging_level(level: str = "info") -> None: +def _get_level_int(level: str) -> int: + """Get the integer corresponding to the level string.""" + level = level.lower() + if level not in level_map: + raise ConfigError( + f"logging level {level} not supported, " f"must be in {list(level_map.keys())}." + ) + return level_map[level] + + +def set_logging_level(level: str = DEFAULT_LEVEL.lower()) -> None: """Set tidy3d logging level priority. Parameters ---------- - level : str + level : str = 'info' + The lowest priority level of logging messages to display. One of ``{'debug', 'info', 'warning', 'error'}`` (listed in increasing priority). - the lowest priority level of logging messages to display. + + Example + ------- + >>> log.debug('this message should not appear (default logging level = INFO') + >>> set_logging_level('debug') + >>> log.debug('this message should appear now') """ - if level not in level_map: - raise ConfigError(f"logging level {level} not supported, must be in {level_map.keys()}.") - level_int = level_map[level] + + level_int = _get_level_int(level) log.setLevel(level_int) + + +def set_logging_file(fname: str, filemode="w", level=DEFAULT_LEVEL.lower()): + """Set a file to write log to, independently from the stdout and stderr + output chosen using :meth:`logging_level`. + + Parameters + ---------- + fname : str + Path to file to direct the output to. + filemode : str = 'w' + 'w' or 'a', defining if the file should be overwritten or appended. + level : str = 'info' + One of 'debug', 'info', 'warning', 'error', 'critical'. This is + set for the file independently of the console output level set by + :meth:`logging_level`. + + Example + ------- + >>> set_logging_file('tidy3d_log.log) + >>> log.warning('this warning will appear in the tidy3d_log.log') + """ + + file_handler = logging.FileHandler(fname, filemode) + level_int = _get_level_int(level) + file_handler.setLevel(level_int) + formatter = logging.Formatter("%(levelname)s: %(message)s") + file_handler.setFormatter(formatter) + log.addHandler(file_handler)