-
Notifications
You must be signed in to change notification settings - Fork 66
feat(autograd): extend gaussian filter padding and inverse design support #2958
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(autograd): extend gaussian filter padding and inverse design support #2958
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additional Comments (1)
-
tidy3d/plugins/autograd/invdes/filters.py, line 291-297 (link)style: Inconsistent docstring formatting:
conicuses double backticks whilecircularandgaussianuse single backticksContext Used: Rule from
dashboard- Adhere to the established docstring format and use correct syntax (e.g., double backticks for code) ... (source)
9 files reviewed, 2 comments
4bd339c to
6a0b965
Compare
Diff CoverageDiff: origin/develop...HEAD, staged and unstaged changes
Summary
tidy3d/plugins/autograd/invdes/filters.pyLines 168-185 168 )
169
170 @staticmethod
171 def get_kernel(size_px: Iterable[int], normalize: bool) -> NDArray:
! 172 raise NotImplementedError("GaussianFilter does not build an explicit kernel.")
173
174 def _apply_filter(self, array: NDArray, size_px: tuple[int, ...]) -> NDArray:
175 radius_px = np.maximum((np.array(size_px, dtype=float) - 1.0) / 2.0, 0.0)
176 if radius_px.size == 0:
! 177 return array
178
179 mode = _GAUSSIAN_PADDING_MAP.get(self.padding)
180 if mode is None:
! 181 raise ValueError(
182 f"Unsupported padding mode '{self.padding}' for gaussian filter; "
183 f"supported modes are {tuple(_GAUSSIAN_PADDING_MAP)}."
184 )Lines 184-192 184 )
185
186 sigma = tuple(float(self.sigma_scale * r) if r > 0 else 0.0 for r in radius_px)
187 if not any(sigma):
! 188 return array
189
190 kwargs: dict[str, Any] = {"mode": mode, "truncate": float(self.truncate)}
191 if mode == "constant":
192 kwargs["cval"] = 0.0tidy3d/plugins/autograd/primitives/misc.pyLines 11-22 11
12 def _normalize_sequence(value: float | Sequence[float], ndim: int) -> tuple[float, ...]:
13 """Convert a scalar or sequence into a tuple of length ``ndim``."""
14 if isinstance(value, Iterable) and not np.isscalar(value):
! 15 value_tuple = tuple(value)
! 16 if len(value_tuple) != ndim:
! 17 raise ValueError(f"Sequence length {len(value_tuple)} does not match ndim {ndim}.")
! 18 return tuple(float(v) for v in value_tuple)
19 return (float(value),) * ndim
20
21
22 def _normalize_modes(mode: str | Sequence[str], ndim: int) -> tuple[str, ...]:Lines 22-33 22 def _normalize_modes(mode: str | Sequence[str], ndim: int) -> tuple[str, ...]:
23 """Normalize a padding mode argument into a tuple of strings with length ``ndim``."""
24 if isinstance(mode, str):
25 return (mode,) * ndim
! 26 mode_tuple = tuple(mode)
! 27 if len(mode_tuple) != ndim:
! 28 raise ValueError(f"Mode sequence length {len(mode_tuple)} does not match ndim {ndim}.")
! 29 return mode_tuple
30
31
32 @cache
33 def _gaussian_weight_matrix(Lines 39-47 39 cval: float,
40 ) -> np.ndarray:
41 """Return the 1-D Gaussian filter matrix used along a single axis."""
42 if sigma <= 0.0:
! 43 return np.eye(length)
44 eye = np.eye(length, dtype=float)
45 weights = scipy.ndimage.gaussian_filter1d(
46 eye,
47 sigma=sigma,Lines 86-96 86 cval_seq = _normalize_sequence(cval, ndim)
87 mode_seq = _normalize_modes(mode, ndim)
88
89 if any(int(o) != 0 for o in order_seq):
! 90 raise NotImplementedError("gaussian_filter VJP currently supports only order=0.")
91 if kwargs:
! 92 raise NotImplementedError(
93 f"gaussian_filter VJP does not support additional keyword arguments: {tuple(kwargs)}"
94 )
95
96 def vjp(g):Lines 97-105 97 grad = np.asarray(g)
98 for axis in reversed(range(ndim)):
99 sigma_axis = float(sigma_seq[axis])
100 if sigma_axis <= 0.0:
! 101 continue
102 mode_axis = mode_seq[axis]
103 truncate_axis = float(truncate_seq[axis])
104 order_axis = int(order_seq[axis])
105 cval_axis = float(cval_seq[axis]) |
a486e1b to
03e909c
Compare
groberts-flex
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is great! overall, left one comment and a clarification to make sure I'm understanding correctly what's going on.
03e909c to
6df6b42
Compare
Pull Request is not mergeable
Our inverse design helpers like the
ErosionDilationPenaltycurrently depend on the two filter types we implement, conic and circular. Neither of these are separable, and combined with the somewhat slow convolve implementation in autograd, this can be a big bottleneck depending on kernel size and filter volume. While we've had a Gaussian filter primitive for a while, it didn't support all padding modes (in the VJP) and hence wasn't used in those inverse design helpers. This PR adds the missing VJP implementations and threads the now-supported Gaussian filter through those routines.Here is a quick benchmark (this includes fwd+bwd passes):

Greptile Overview
Updated On: 2025-11-04 12:51:40 UTC
Greptile Summary
This PR extends Gaussian filter support in the autograd inverse design system by implementing VJP (vector-jacobian product) gradients for all padding modes.
Key Changes:
gaussian_filterprimitive supporting all padding modes:constant,nearest,mirror,reflect, andwrap(tidy3d/plugins/autograd/primitives/misc.py:79-117)GaussianFilterclass to inverse design filters with padding mode mapping and configurable sigma scaling (tidy3d/plugins/autograd/invdes/filters.py:144-194)sigma_scaleof 0.445 empirically tuned to match conic kernel responseKernelTypeto include"gaussian"option alongside existing"circular"and"conic"typesMinor Issues Found:
normalizeparameter inherited fromAbstractFilteris ignored byGaussianFilterbut not documented in the class docstringThe implementation correctly integrates with existing inverse design components like
FilterAndProjectandErosionDilationPenalty, enabling faster filtering operations through separable Gaussian kernels.Confidence Score: 4/5
Important Files Changed
File Analysis
Sequence Diagram
sequenceDiagram participant User participant FilterAndProject participant GaussianFilter participant gaussian_filter_primitive participant scipy_gaussian_filter participant VJP User->>FilterAndProject: __call__(array) FilterAndProject->>GaussianFilter: make_filter(filter_type="gaussian") GaussianFilter->>GaussianFilter: _apply_filter(array, size_px) GaussianFilter->>GaussianFilter: map padding mode (edge→nearest, symmetric→mirror) GaussianFilter->>GaussianFilter: compute sigma from radius_px and sigma_scale GaussianFilter->>gaussian_filter_primitive: gaussian_filter(array, sigma, mode, truncate, cval) gaussian_filter_primitive->>scipy_gaussian_filter: forward pass scipy_gaussian_filter-->>gaussian_filter_primitive: filtered result gaussian_filter_primitive-->>GaussianFilter: filtered array GaussianFilter-->>FilterAndProject: filtered array FilterAndProject->>FilterAndProject: tanh_projection(filtered, beta, eta) FilterAndProject-->>User: projected result Note over User,VJP: Backward Pass (Gradient Computation) User->>VJP: grad output VJP->>VJP: _gaussian_filter_vjp() VJP->>VJP: compute weight matrices per axis loop For each axis (reversed) VJP->>VJP: _gaussian_weight_matrix(length, sigma, mode, truncate, order, cval) VJP->>VJP: tensordot(weights.T, grad) end VJP-->>User: input gradient