Source code for tidy3d.plugins.autograd.invdes.projections

from __future__ import annotations

import autograd.numpy as np
from numpy.typing import NDArray

from tidy3d.plugins.autograd.constants import BETA_DEFAULT, ETA_DEFAULT


[docs] def ramp_projection(array: NDArray, width: float = 0.1, center: float = 0.5) -> NDArray: """Apply a piecewise linear ramp projection to an array. This function performs a ramp projection on the input array, modifying its values based on the specified width and center. Values within the range [center - width/2, center + width/2] are linearly transformed, while values outside this range are projected to 0 or 1. The input and output is assumed to be within the range [0, 1]. Parameters ---------- array : np.ndarray The input array to be projected. width : float = 0.1 The width of the ramp. center : float 0.5 The center of the ramp. Returns ------- np.ndarray The array after applying the ramp projection. """ ll = array <= (center - width / 2) cc = (array > (center - width / 2)) & (array < (center + width / 2)) rr = array >= (center + width / 2) return np.concatenate( [ np.zeros(array[ll].size), (array[cc] - (center - width / 2)) / width, np.ones(array[rr].size), ] )
[docs] def tanh_projection( array: NDArray, beta: float = BETA_DEFAULT, eta: float = ETA_DEFAULT ) -> NDArray: """Apply a tanh-based soft-thresholding projection to an array. This function performs a tanh projection on the input array, which is a common soft-thresholding scheme used in topology optimization. The projection modifies the values of the array based on the specified `beta` and `eta` parameters. Parameters ---------- array : np.ndarray The input array to be projected. beta : float = BETA_DEFAULT The steepness of the projection. Higher values result in a sharper transition. eta : float = ETA_DEFAULT The midpoint of the projection. Returns ------- np.ndarray The array after applying the tanh projection. """ if beta == 0: return array if beta == np.inf: return np.where(array > eta, 1.0, 0.0) num = np.tanh(beta * eta) + np.tanh(beta * (array - eta)) denom = np.tanh(beta * eta) + np.tanh(beta * (1 - eta)) return num / denom
[docs] def smoothed_projection( array: NDArray, beta: float = BETA_DEFAULT, eta: float = ETA_DEFAULT, scaling_factor=1.0, ) -> NDArray: """ Apply a subpixel-smoothed projection method. The subpixel-smoothed projection method is discussed in [1]_ as follows: This projection method eliminates discontinuities by applying first-order smoothing at material boundaries through analytical fill factors. Unlike traditional quadrature approaches, it works with maximum projection strength (:math:`\\beta = \\infty`) and derives closed-form expressions for interfacial regions. Prerequisites: input fields must be pre-filtered for continuity (for example using a conic filter). The algorithm detects whether boundaries intersect grid cells. When interfaces are absent, standard projection is applied. For cells containing boundaries, analytical fill ratios are computed to maintain gradient continuity as interfaces move through cells and traverse pixel centers. This enables arbitrarily large :math:`\\beta` values while preserving differentiability throughout the transition process. .. warning:: This function assumes that the device is placed on a uniform grid. When using ```GridSpec.auto``` in the simulation, make sure to place a ``MeshOverrideStructure`` at the position of the optimized geometry. Parameters ---------- array : np.ndarray The input array to be projected. beta : float = BETA_DEFAULT The steepness of the projection. Higher values result in a sharper transition. eta : float = ETA_DEFAULT The midpoint of the projection. scaling_factor: float = 1.0 Optional scaling factor to adjust dx and dy to different resolutions. Example ------- >>> import autograd.numpy as np >>> from tidy3d.plugins.autograd.invdes.filters import ConicFilter >>> arr = np.random.uniform(size=(50, 50)) >>> filter = ConicFilter(kernel_size=5) >>> arr_filtered = filter(arr) >>> eta = 0.5 # center of projection >>> smoothed = smoothed_projection(arr_filtered, beta=np.inf, eta=eta) .. [1] A. M. Hammond, A. Oskooi, I. M. Hammond, M. Chen, S. E. Ralph, and S. G. Johnson, "Unifying and accelerating level-set and density-based topology optimization by subpixel-smoothed projection," arXiv:2503.20189v3 [physics.optics] (2025). """ # sanity checks if array.ndim != 2: raise ValueError(f"Smoothed projection expects a 2d-array, but got shape {array.shape=}") # smoothing kernel is circle (or ellipse for non-uniform grid) # we choose smoothing kernel with unit area, which is r~=0.56, a bit larger than (arbitrary) default r=0.55 in paper dx = dy = scaling_factor smooth_radius = np.sqrt(1 / np.pi) * scaling_factor original_projected = tanh_projection(array, beta=beta, eta=eta) # finite-difference spatial gradients rho_filtered_grad = np.gradient(array) rho_filtered_grad_helper = (rho_filtered_grad[0] / dx) ** 2 + (rho_filtered_grad[1] / dy) ** 2 nonzero_norm = np.abs(rho_filtered_grad_helper) > 1e-10 filtered_grad_norm = np.sqrt(np.where(nonzero_norm, rho_filtered_grad_helper, 1)) filtered_grad_norm_eff = np.where(nonzero_norm, filtered_grad_norm, 1) # distance of pixel center to nearest interface distance = (eta - array) / filtered_grad_norm_eff needs_smoothing = nonzero_norm & (np.abs(distance) < smooth_radius) # double where trick d_rel = distance / smooth_radius polynom = np.where( needs_smoothing, 0.5 - 15 / 16 * d_rel + 5 / 8 * d_rel**3 - 3 / 16 * d_rel**5, 1.0 ) # F(-d) polynom_neg = np.where( needs_smoothing, 0.5 + 15 / 16 * d_rel - 5 / 8 * d_rel**3 + 3 / 16 * d_rel**5, 1.0 ) # two projections, one for lower and one for upper bound rho_filtered_minus = array - smooth_radius * filtered_grad_norm_eff * polynom rho_filtered_plus = array + smooth_radius * filtered_grad_norm_eff * polynom_neg rho_minus_eff_projected = tanh_projection(rho_filtered_minus, beta=beta, eta=eta) rho_plus_eff_projected = tanh_projection(rho_filtered_plus, beta=beta, eta=eta) # Smoothing is only applied to projections projected_smoothed = (1 - polynom) * rho_minus_eff_projected + polynom * rho_plus_eff_projected smoothed = np.where( needs_smoothing, projected_smoothed, original_projected, ) return smoothed