@@ -70,3 +70,107 @@ def tanh_projection(
7070 num = np .tanh (beta * eta ) + np .tanh (beta * (array - eta ))
7171 denom = np .tanh (beta * eta ) + np .tanh (beta * (1 - eta ))
7272 return num / denom
73+
74+
75+ def smoothed_projection (
76+ array : NDArray ,
77+ beta : float = BETA_DEFAULT ,
78+ eta : float = ETA_DEFAULT ,
79+ scaling_factor = 1.0 ,
80+ ) -> NDArray :
81+ """
82+ Apply a subpixel-smoothed projection method.
83+ The subpixel-smoothed projection method is discussed in [1]_ as follows:
84+
85+ This projection method eliminates discontinuities by applying first-order
86+ smoothing at material boundaries through analytical fill factors. Unlike
87+ traditional quadrature approaches, it works with maximum projection strength
88+ (:math:`\\ beta = \\ infty`) and derives closed-form expressions for interfacial regions.
89+
90+ Prerequisites: input fields must be pre-filtered for continuity (for example
91+ using a conic filter).
92+
93+ The algorithm detects whether boundaries intersect grid cells. When interfaces
94+ are absent, standard projection is applied. For cells containing boundaries,
95+ analytical fill ratios are computed to maintain gradient continuity as interfaces
96+ move through cells and traverse pixel centers. This enables arbitrarily large
97+ :math:`\\ beta` values while preserving differentiability throughout the transition
98+ process.
99+
100+ Parameters
101+ ----------
102+ array : np.ndarray
103+ The input array to be projected.
104+ beta : float = BETA_DEFAULT
105+ The steepness of the projection. Higher values result in a sharper transition.
106+ eta : float = ETA_DEFAULT
107+ The midpoint of the projection.
108+ scaling_factor: float = 1.0
109+ Optional scaling factor to adjust dx and dy to different resolutions.
110+
111+ Example
112+ -------
113+ >>> import autograd.numpy as np
114+ >>> from tidy3d.plugins.autograd.invdes.filters import ConicFilter
115+ >>> arr = np.random.uniform(size=(50, 50))
116+ >>> filter = ConicFilter(kernel_size=5)
117+ >>> arr_filtered = filter(arr)
118+ >>> eta = 0.5 # center of projection
119+ >>> smoothed = smoothed_projection(arr_filtered, beta=np.inf, eta=eta)
120+
121+ .. [1] A. M. Hammond, A. Oskooi, I. M. Hammond, M. Chen, S. E. Ralph, and
122+ S. G. Johnson, "Unifying and accelerating level-set and density-based topology
123+ optimization by subpixel-smoothed projection," arXiv:2503.20189v3 [physics.optics]
124+ (2025).
125+ """
126+ # sanity checks
127+ assert array .ndim == 2
128+ if np .isinf (beta ) and np .any (np .isclose (array , eta )):
129+ raise Exception (
130+ "For beta of infinity no value in the array should be close to eta, otherwise nan-values will emerge"
131+ )
132+ # smoothing kernel is circle (or ellipse for non-uniform grid)
133+ # we choose smoothing kernel with unit area, which is r~=0.56, a bit larger than (arbitrary) default r=0.55 in paper
134+ dx = dy = scaling_factor
135+ smooth_radius = np .sqrt (1 / np .pi ) * scaling_factor
136+
137+ original_projected = tanh_projection (array , beta = beta , eta = eta )
138+
139+ # finite-difference spatial gradients
140+ rho_filtered_grad = np .gradient (array )
141+ rho_filtered_grad_helper = (rho_filtered_grad [0 ] / dx ) ** 2 + (rho_filtered_grad [1 ] / dy ) ** 2
142+
143+ nonzero_norm = np .abs (rho_filtered_grad_helper ) > 1e-10
144+
145+ filtered_grad_norm = np .sqrt (np .where (nonzero_norm , rho_filtered_grad_helper , 1 ))
146+ filtered_grad_norm_eff = np .where (nonzero_norm , filtered_grad_norm , 1 )
147+
148+ # distance of pixel center to nearest interface
149+ distance = (eta - array ) / filtered_grad_norm_eff
150+
151+ needs_smoothing = nonzero_norm & (np .abs (distance ) < smooth_radius )
152+
153+ # double where trick
154+ d_rel = distance / smooth_radius
155+ polynom = np .where (
156+ needs_smoothing , 0.5 - 15 / 16 * d_rel + 5 / 8 * d_rel ** 3 - 3 / 16 * d_rel ** 5 , 1.0
157+ )
158+ # F(-d)
159+ polynom_neg = np .where (
160+ needs_smoothing , 0.5 + 15 / 16 * d_rel - 5 / 8 * d_rel ** 3 + 3 / 16 * d_rel ** 5 , 1.0
161+ )
162+
163+ # two projections, one for lower and one for upper bound
164+ rho_filtered_minus = array - smooth_radius * filtered_grad_norm_eff * polynom
165+ rho_filtered_plus = array + smooth_radius * filtered_grad_norm_eff * polynom_neg
166+ rho_minus_eff_projected = tanh_projection (rho_filtered_minus , beta = beta , eta = eta )
167+ rho_plus_eff_projected = tanh_projection (rho_filtered_plus , beta = beta , eta = eta )
168+
169+ # Smoothing is only applied to projections
170+ projected_smoothed = (1 - polynom ) * rho_minus_eff_projected + polynom * rho_plus_eff_projected
171+ smoothed = np .where (
172+ needs_smoothing ,
173+ projected_smoothed ,
174+ original_projected ,
175+ )
176+ return smoothed
0 commit comments