Skip to content

Commit

Permalink
attempted to add monitors that depend on the grid automatically
Browse files Browse the repository at this point in the history
  • Loading branch information
dmarek-flex committed Nov 13, 2024
1 parent 969a823 commit 85b0439
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 22 deletions.
2 changes: 1 addition & 1 deletion tests/test_components/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,7 @@ def test_nyquist():
# fake a scenario where the fmax of the simulation is negative?
class MockSim:
frequency_range = (-2, -1)
monitors = ()
_internal_monitors = ()
_cached_properties = {}

m = MockSim()
Expand Down
10 changes: 8 additions & 2 deletions tidy3d/components/base_sim/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ def scene(self) -> Scene:

def get_monitor_by_name(self, name: str) -> AbstractMonitor:
"""Return monitor named 'name'."""
for monitor in self.monitors:
for monitor in self._internal_monitors:
if monitor.name == name:
return monitor
raise Tidy3dKeyError(f"No monitor named '{name}'")
Expand Down Expand Up @@ -361,7 +361,7 @@ def plot_monitors(
The supplied or created matplotlib axes.
"""
bounds = self.bounds
for monitor in self.monitors:
for monitor in self._internal_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
Expand Down Expand Up @@ -671,3 +671,9 @@ def from_scene(cls, scene: Scene, **kwargs) -> AbstractSimulation:
medium=scene.medium,
**kwargs,
)

@cached_property
def _internal_monitors(self) -> Tuple[None, ...]:
"""Generate a tuple of all monitors wherein any monitors that are generated for
internal use are added to the list of user-supplied monitors."""
return self.monitors
24 changes: 16 additions & 8 deletions tidy3d/components/lumped_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from ..components.monitor import FieldMonitor
from ..components.structure import MeshOverrideStructure, Structure
from ..components.validators import assert_plane, validate_name_str
from ..constants import EPSILON_0, FARAD, HENRY, MICROMETER, OHM
from ..constants import EPSILON_0, FARAD, HENRY, HERTZ, MICROMETER, OHM
from ..exceptions import ValidationError
from .base import Tidy3dBaseModel, cached_property, skip_if_fields_missing
from .geometry.base import Box, ClipOperation, Geometry
Expand Down Expand Up @@ -96,6 +96,14 @@ class RectangularLumpedElement(LumpedElement, Box):
"boundary along their ``normal_axis``, regardless of this option.",
)

monitor_freqs: Optional[FreqArray] = pd.Field(
None,
title="Monitor Frequencies",
description="When set, monitors are added to the simulation that enable the calculation of the"
"voltage and current at the specified frequencies.",
units=HERTZ,
)

@cached_property
def normal_axis(self):
"""Normal axis of the lumped element, which is the axis where the element has zero size."""
Expand Down Expand Up @@ -187,7 +195,7 @@ def to_mesh_overrides(self) -> list[MeshOverrideStructure]:
def to_geometry(self, grid: Grid = None) -> Box:
"""Converts the :class:`RectangularLumpedElement` object to a :class:`.Box`."""
box = Box(size=self.size, center=self.center)
if grid and self.snap_perimeter_to_grid:
if grid is not None and self.snap_perimeter_to_grid:
return snap_box_to_grid(grid, box, self._snapping_spec)
return box

Expand Down Expand Up @@ -216,25 +224,25 @@ def _admittance_transfer_function_scaling(self, box: Box = None) -> float:
# The final scaling along the normal axis is applied when the resulting 2D medium is averaged with the background media.
return size_voltage / size_lateral

def to_monitor(self, freqs: FreqArray) -> FieldMonitor:
def to_monitor(self, freqs: FreqArray, grid: Grid = None) -> FieldMonitor:
"""Creates a field monitor that can be added to the simulation, which records field data
that can be used to later compute voltage and current flowing through the element.
"""
box = self.to_geometry(grid)

center = list(self.center)
# Size of monitor needs to be nonzero along the normal axis so that the magnetic field on
# both sides of the sheet will be available
mon_size = list(self.size)
# both sides of the sheet will be available. If snapping is used,
mon_size = list(box.size)
mon_size[self.normal_axis] = 2 * (
increment_float(center[self.normal_axis], 1.0) - center[self.normal_axis]
increment_float(box.center[self.normal_axis], 1.0) - box.center[self.normal_axis]
)

e_component = "xyz"[self.voltage_axis]
h1_component = "xyz"[self.lateral_axis]
h2_component = "xyz"[self.normal_axis]
# Create a voltage monitor
return FieldMonitor(
center=center,
center=box.center,
size=mon_size,
freqs=freqs,
fields=[f"E{e_component}", f"H{h1_component}", f"H{h2_component}"],
Expand Down
46 changes: 35 additions & 11 deletions tidy3d/components/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,10 @@
from .structure import MeshOverrideStructure, Structure
from .subpixel_spec import SubpixelSpec
from .types import TYPE_TAG_STR, Ax, Axis, FreqBound, InterpMethod, Literal, Symmetry, annotate_type
from .validators import assert_objects_in_sim_bounds, validate_mode_objects_symmetry
from .validators import (
assert_objects_in_sim_bounds,
validate_mode_objects_symmetry,
)
from .viz import (
PlotParams,
add_ax_if_none,
Expand Down Expand Up @@ -1286,7 +1289,7 @@ def snap_to_grid(geom: Geometry, axis: Axis) -> Geometry:
snapped_center = snap_coordinate_to_grid(self.grid, center, axis)
return geom._update_from_bounds(bounds=(snapped_center, snapped_center), axis=axis)

# Convert lumped elements into structures
# Convert lumped elements into structures and add monitors if desired
lumped_structures = []
for lumped_element in self.lumped_elements:
lumped_structures.append(lumped_element.to_structure(self.grid))
Expand Down Expand Up @@ -1344,6 +1347,27 @@ def volumetric_structures(self) -> Tuple[Structure]:
volumetric equivalents."""
return self._volumetric_structures_grid(self.grid)

def _internal_monitors_grid(self, grid: Grid) -> Tuple[Monitor]:
"""Generate a tuple of all monitors wherein any monitors that are generated for
internal use are added to the list of user-supplied monitors."""
# Add monitors around lumped elements if desired
internal_monitors = list(self.monitors)
for lumped_element in self.lumped_elements:
if lumped_element.monitor_freqs is not None:
internal_monitors.append(
lumped_element.to_monitor(freqs=lumped_element.monitor_freqs, grid=grid)
)
# Would be nice to be able to run these validators on the generated monitors.
# _unique_monitor_names = assert_unique_names("monitors")
# _monitors_in_bounds = assert_objects_in_sim_bounds("monitors", strict_inequality=True)
return tuple(internal_monitors)

@cached_property
def _internal_monitors(self) -> Tuple[Monitor]:
"""Generate a tuple of all monitors wherein any monitors that are generated for
internal use are added to the list of user-supplied monitors."""
return self._internal_monitors_grid(self.grid)

def suggest_mesh_overrides(self, **kwargs) -> List[MeshOverrideStructure]:
"""Generate a :class:.`MeshOverrideStructure` `List` which is automatically generated
from structures in the simulation.
Expand Down Expand Up @@ -3395,7 +3419,7 @@ def _validate_monitor_size(self) -> None:

# 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:
for monitor in self._internal_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
Expand Down Expand Up @@ -3439,7 +3463,7 @@ def warn_mode_size(monitor: AbstractModeMonitor, msg_header: str, custom_loc: Li
warn_mode_size(monitor=monitor, msg_header=msg_header, custom_loc=custom_loc)

with log as consolidated_logger:
for mnt_ind, monitor in enumerate(self.monitors):
for mnt_ind, monitor in enumerate(self._internal_monitors):
if isinstance(monitor, AbstractModeMonitor):
msg_header = f"Mode monitor '{monitor.name}' "
custom_loc = ["monitors", mnt_ind]
Expand Down Expand Up @@ -3470,14 +3494,14 @@ def check_num_cells(
msg_header = f"Mode source '{source.name}' "
check_num_cells(source, source.injection_axis, msg_header)

for monitor in self.monitors:
for monitor in self._internal_monitors:
if isinstance(monitor, ModeMonitor):
msg_header = f"Mode monitor '{monitor.name}' "
check_num_cells(monitor, monitor.normal_axis, msg_header)

def _validate_time_monitors_num_steps(self) -> None:
"""Raise an error if non-0D time monitors have too many time steps."""
for monitor in self.monitors:
for monitor in self._internal_monitors:
if not isinstance(monitor, FieldTimeMonitor) or len(monitor.zero_dims) == 3:
continue
num_time_steps = monitor.num_steps(self.tmesh)
Expand All @@ -3499,7 +3523,7 @@ def static_structures(self) -> list[Structure]:
def monitors_data_size(self) -> Dict[str, float]:
"""Dictionary mapping monitor names to their estimated storage size in bytes."""
data_size = {}
for monitor in self.monitors:
for monitor in self._internal_monitors:
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
Expand Down Expand Up @@ -3601,7 +3625,7 @@ def _warn_time_monitors_outside_run_time(self) -> None:
dynamically computed run_time e.g. through a ``_run_time`` cached property.
"""
with log as consolidated_logger:
for monitor in self.monitors:
for monitor in self._internal_monitors:
if isinstance(monitor, TimeMonitor) and monitor.start > self._run_time:
consolidated_logger.warning(
f"Monitor {monitor.name} has a start time {monitor.start:1.2e}s exceeding"
Expand All @@ -3617,7 +3641,7 @@ def with_adjoint_monitors(self, sim_fields_keys: list) -> Simulation:
structure_indices = {index for (_, index, *_) in sim_fields_keys}

mnts_fld, mnts_eps = self.make_adjoint_monitors(structure_indices=structure_indices)
monitors = list(self.monitors) + list(mnts_fld) + list(mnts_eps)
monitors = list(self._internal_monitors) + list(mnts_fld) + list(mnts_eps)
return self.copy(update=dict(monitors=monitors))

def make_adjoint_monitors(self, structure_indices: set[int]) -> tuple[list, list]:
Expand All @@ -3644,7 +3668,7 @@ def freqs_adjoint(self) -> list[float]:
"""Unique list of all frequencies. For now should be only one."""

freqs = set()
for mnt in self.monitors:
for mnt in self._internal_monitors:
if isinstance(mnt, FreqMonitor):
freqs.update(mnt.freqs)
freqs = sorted(freqs)
Expand Down Expand Up @@ -4372,7 +4396,7 @@ def nyquist_step(self) -> int:
freq_monitor_max = max(
(
monitor.frequency_range[1]
for monitor in self.monitors
for monitor in self._internal_monitors
if isinstance(monitor, FreqMonitor) and not isinstance(monitor, PermittivityMonitor)
),
default=0.0,
Expand Down

0 comments on commit 85b0439

Please sign in to comment.