From 2b87a377bedb943999ee3a96c7395ccb8a324681 Mon Sep 17 00:00:00 2001 From: Yannick Augenstein Date: Tue, 19 Aug 2025 12:44:43 +0200 Subject: [PATCH] poc: adjoint surface monitors --- tidy3d/components/data/data_array.py | 42 ++++++++++++++++ tidy3d/components/data/dataset.py | 68 ++++++++++++++++++++++++++ tidy3d/components/data/monitor_data.py | 39 +++++++++++++++ tidy3d/components/monitor.py | 54 ++++++++++++++++++++ 4 files changed, 203 insertions(+) diff --git a/tidy3d/components/data/data_array.py b/tidy3d/components/data/data_array.py index 1cbae1d8e4..11041e1fe9 100644 --- a/tidy3d/components/data/data_array.py +++ b/tidy3d/components/data/data_array.py @@ -58,6 +58,8 @@ "face_index": {"long_name": "face index"}, "vertex_index": {"long_name": "vertex index"}, "axis": {"long_name": "axis"}, + "sample": {"long_name": "surface sample index"}, + "side": {"long_name": "surface side"}, } @@ -1032,6 +1034,46 @@ class TriangleMeshDataArray(DataArray): _data_attrs = {"long_name": "surface mesh triangles"} +class SurfaceScalarDataArray(DataArray): + """Scalar field samples recorded on geometry surfaces. + + Dims: (sample, f, side) + + - ``sample``: index of a unique surface sample point. + - ``f``: frequency in Hz. + - ``side``: surface side label, typically ``['inside', 'outside']``. + """ + + __slots__ = () + _dims = ("sample", "f", "side") + _data_attrs = {"long_name": "surface scalar samples"} + + +class SurfaceVectorDataArray(DataArray): + """Vector-valued quantity per surface sample (e.g., coordinates or normals). + + Dims: (sample, axis) + + - ``sample``: index of surface sample. + - ``axis``: x=0, y=1, z=2. + """ + + __slots__ = () + _dims = ("sample", "axis") + _data_attrs = {"long_name": "surface vector components"} + + +class SampleWeightDataArray(DataArray): + """Quadrature weight or differential area associated with a surface sample. + + Dims: (sample,) + """ + + __slots__ = () + _dims = ("sample",) + _data_attrs = {"long_name": "surface sample weight"} + + class HeatDataArray(DataArray): """Heat data array. diff --git a/tidy3d/components/data/dataset.py b/tidy3d/components/data/dataset.py index 697a5d4805..18926534d8 100644 --- a/tidy3d/components/data/dataset.py +++ b/tidy3d/components/data/dataset.py @@ -22,10 +22,13 @@ GroupIndexDataArray, ModeDispersionDataArray, ModeIndexDataArray, + SampleWeightDataArray, ScalarFieldDataArray, ScalarFieldTimeDataArray, ScalarModeFieldCylindricalDataArray, ScalarModeFieldDataArray, + SurfaceScalarDataArray, + SurfaceVectorDataArray, TimeDataArray, TriangleMeshDataArray, ) @@ -675,6 +678,71 @@ class TriangleMeshDataset(Dataset): ) +class SurfaceSamplesDataset(Dataset): + """Geometry surface samples and associated geometric metadata. + + - ``points``: Cartesian coordinates of each surface sample. + - ``normals``: Outward-pointing unit normals at each sample. + - ``perp1`` and ``perp2``: Optional orthonormal tangents spanning the local tangent plane. + If not provided, we can construct a consistent basis from ``normals``. + - ``weights``: Quadrature weights (e.g., differential area per sample). + """ + + points: SurfaceVectorDataArray = pd.Field( + ..., title="Sample points", description="Sample coordinates (x,y,z) per surface sample." + ) + normals: SurfaceVectorDataArray = pd.Field( + ..., title="Normals", description="Outward surface normals at each sample." + ) + weights: SampleWeightDataArray = pd.Field( + ..., title="Weights", description="Quadrature weight or area per surface sample." + ) + perp1: SurfaceVectorDataArray = pd.Field( + None, + title="Tangent 1", + description="Optional first tangent vector per sample; orthonormal to the normal.", + ) + perp2: SurfaceVectorDataArray = pd.Field( + None, + title="Tangent 2", + description="Optional second tangent vector; completes an orthonormal basis.", + ) + + +class AdjointDielectricSurfaceComponents(Dataset): + """Projected components recorded for dielectric adjoint surface VJP.""" + + samples: SurfaceSamplesDataset = pd.Field( + ..., title="Surface samples", description="Geometry surface samples and metadata." + ) + Et1: SurfaceScalarDataArray = pd.Field( + None, title="Et1", description="First tangential component of E at samples." + ) + Et2: SurfaceScalarDataArray = pd.Field( + None, title="Et2", description="Second tangential component of E at samples." + ) + Dn: SurfaceScalarDataArray = pd.Field( + None, title="Dn", description="Normal component of D at samples." + ) + + +class AdjointPECSurfaceComponents(Dataset): + """Projected components recorded for PEC adjoint surface VJP.""" + + samples: SurfaceSamplesDataset = pd.Field( + ..., title="Surface samples", description="Geometry surface samples and metadata." + ) + En: SurfaceScalarDataArray = pd.Field( + None, title="En", description="Normal component of E at samples (outside PEC)." + ) + Ht1: SurfaceScalarDataArray = pd.Field( + None, title="Ht1", description="First tangential component of H at samples (outside PEC)." + ) + Ht2: SurfaceScalarDataArray = pd.Field( + None, title="Ht2", description="Second tangential component of H at samples (outside PEC)." + ) + + class TimeDataset(Dataset): """Dataset for storing a function of time.""" diff --git a/tidy3d/components/data/monitor_data.py b/tidy3d/components/data/monitor_data.py index bfd0a9a0df..119a70ef2c 100644 --- a/tidy3d/components/data/monitor_data.py +++ b/tidy3d/components/data/monitor_data.py @@ -82,6 +82,8 @@ ) from .dataset import ( AbstractFieldDataset, + AdjointDielectricSurfaceComponents, + AdjointPECSurfaceComponents, AuxFieldTimeDataset, Dataset, ElectromagneticFieldDataset, @@ -3907,6 +3909,41 @@ def fields_circular_polarization(self) -> xr.Dataset: return xr.Dataset(dict(zip(keys, data_arrays))) +class AdjointDielectricSurfaceData(AdjointDielectricSurfaceComponents, MonitorData): + """ + Data recorded by a dielectric adjoint surface monitor. + + Fields + - samples: SurfaceSamplesDataset + - Et1: SurfaceScalarDataArray + - Et2: SurfaceScalarDataArray + - Dn: SurfaceScalarDataArray + """ + + monitor: MonitorType = pd.Field( + ..., + title="Monitor", + description="Dielectric adjoint surface monitor associated with the data.", + ) + + +class AdjointPECSurfaceData(AdjointPECSurfaceComponents, MonitorData): + """ + Data recorded by a PEC adjoint surface monitor. + + Fields + - samples: SurfaceSamplesDataset + - En: SurfaceScalarDataArray + - Ht1: SurfaceScalarDataArray + - Ht2: SurfaceScalarDataArray + """ + + monitor: MonitorType = pd.Field( + ..., title="Monitor", description="PEC adjoint surface monitor associated with the data." + ) + + +# Register all monitor data types after class definitions MonitorDataTypes = ( FieldData, FieldTimeData, @@ -3921,6 +3958,8 @@ def fields_circular_polarization(self) -> xr.Dataset: FieldProjectionAngleData, DiffractionData, DirectivityData, + AdjointDielectricSurfaceData, + AdjointPECSurfaceData, ) MonitorDataType = Union[MonitorDataTypes] diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index 72833418e3..984b9af45c 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -1543,6 +1543,57 @@ def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int: return BYTES_COMPLEX * num_cells * len(self.freqs) * 6 +class AdjointSurfaceMonitor(FreqMonitor): + """Base class for adjoint shape-optimization surface monitors. + + Records boundary field samples on geometry surfaces within the monitor bounding box. + Concrete subclasses define exactly which components are recorded. + + Parameters + ---------- + sides : tuple['inside'|'outside', ...] + Which sides of the surface to record. Concrete subclasses may constrain this + (e.g., PEC outside-only). + """ + + sides: tuple[Literal["inside", "outside"], ...] = pydantic.Field( + ("inside", "outside"), + title="Sides to record", + description="Which surface sides to record. Concrete monitors may constrain this.", + ) + + colocate: Literal[True] = pydantic.Field( + True, + title="Colocate Fields", + description="Surface sampling is colocated to boundary locations by design.", + ) + + def storage_size(self, num_cells: int, tmesh: ArrayFloat1D) -> int: + """Conservative storage estimate: 3 complex components × freqs × sides × samples. + + Notes + ----- + 'num_cells' is treated as the number of surface samples (post-discretization). + Concrete subclasses may adjust this if needed. + """ + nfreq = len(self.freqs) + nsides = len(self.sides) + ncomp = 3 + return BYTES_COMPLEX * num_cells * nfreq * ncomp * nsides + + +class AdjointDielectricSurfaceMonitor(AdjointSurfaceMonitor): + """Convenience class for dielectric adjoint surface recording.""" + + sides: tuple[Literal["inside", "outside"], ...] = ("inside", "outside") + + +class AdjointPECSurfaceMonitor(AdjointSurfaceMonitor): + """Convenience class for PEC adjoint surface recording.""" + + sides: tuple[Literal["outside"], ...] = ("outside",) + + # types of monitors that are accepted by simulation MonitorType = Union[ FieldMonitor, @@ -1558,4 +1609,7 @@ def _storage_size_solver(self, num_cells: int, tmesh: ArrayFloat1D) -> int: FieldProjectionKSpaceMonitor, DiffractionMonitor, DirectivityMonitor, + AdjointSurfaceMonitor, + AdjointDielectricSurfaceMonitor, + AdjointPECSurfaceMonitor, ]