Fiber lenses, or microlenses fabricated directly on optical fiber tips, are essential components for beam shaping, focusing, and coupling in fiber-optic systems. Applications range from optical trapping and sensing to endoscopy and telecommunications.
In this notebook, we design a fiber lens by optimizing the 3D surface profile of a cylindrically symmetric structure attached to the tip of a single-mode fiber. The lens surface is parameterized as concentric rings whose heights are adjusted via gradient-based inverse design using Tidy3D’s native autograd support. The objective is to maximize the optical flux transmitted to a small focal region in front of the fiber.
We demonstrate free-form 3D shape optimization using a fully differentiable TriangleMesh geometry. Unlike topology optimization on a fixed pixel grid, this approach directly optimizes vertex positions of a surface mesh, enabling smooth, fabrication-ready freeform surfaces that can be exported directly to STL. This capability is particularly powerful for freeform optics where the optimal shape cannot be described by simple geometric primitives.
Initial Setup
We begin by importing the necessary packages and defining the key parameters for our simulation. The physical setup consists of:
Fiber base: A single-mode fiber with core and cladding
Lens structure: A cylinder-like design region whose top surface will be optimized
Exit monitor: A small flux monitor at the target focal plane to measure focusing efficiency
The lens surface is discretized into num_rings concentric rings, each with an adjustable height. A Gaussian smoothing kernel is applied to ensure smooth, fabricable transitions between adjacent rings.
import autograd.numpy as anpimport matplotlib.pyplot as pltimport numpy as npimport tidy3d as tdimport tidy3d.web as webfrom autograd.scipy.signal import convolvefrom tidy3d.components.autograd import get_staticfrom tidy3d.plugins.autograd import adam, optimizetd.config.logging.level ="ERROR"# disable warning about large number of source grid points# Operating wavelength and frequency (units: µm)wvl =1.55freq0 = td.C_0 / wvl# Fiber geometry and refractive indices (approximate SMF-28 values)d_smf_core =8.2# core diameter (µm)n_smf_core =1.449217# core refractive indexn_smf_clad =1.444# cladding refractive indexn_design =2.0# refractive index of lens material (e.g., polymer or glass)base_design_radius =10# radius of the lens base (µm)fiber_length =2# length of fiber section in simulation (µm)# Longitudinal layout (µm)initial_height =2.0# starting height for all ringsfree_space_length =20.0# propagation distance to focal planepml_buffer = wvl *1.5# buffer between structures and PMLmonitor_buffer =0.5# buffer for source/monitor placement# Simulation domain extent (µm)sim_Ly = sim_Lz =2* base_design_radius + pml_buffersim_Lx = fiber_length + free_space_lengthexit_monitor_size =0.5# size of the focal spot monitor# Mesh parameterizationnum_rings =128# number of concentric rings (design parameters)ring_segments =128# polygon edges per ring (angular resolution)smooth_sigma_frac =0.02# smoothing kernel width = num_rings * smooth_sigma_frac# Height bounds for optimization (µm)min_height, max_height =0.1, 10.0
Simulation Setup
We now construct the base simulation containing the optical fiber without the lens structure. The fiber consists of a cylindrical core (higher refractive index) surrounded by cladding (lower refractive index). Both extend beyond the simulation boundaries to avoid edge effects at the PML.
We excite the fundamental mode of the fiber using a ModeSource. The mode solver automatically finds the guided mode, and we filter for TE polarization. The source emits a Gaussian pulse centered at the operating wavelength.
A FluxMonitor is placed at the target focal plane to measure the transmitted optical power directly. The monitor size defines the “focal spot” region where we want to concentrate light.
We now assemble the base simulation with the fiber structures, source, and monitors. Optionally, a field monitor at z=0 can be added to visualize the field propagation along the optical axis.
Let’s visualize the base simulation in 2D cross-sections and 3D to verify the geometry and component placement.
f, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(12, 4))base_sim.plot(x=fiber_length /2, ax=ax1)ax1.set_title(f"Cross-section at x = {fiber_length /2} µm")base_sim.plot(z=0, ax=ax2)ax2.set_title("Top view at z = 0")plt.show()base_sim.plot_3d()
Mesh Construction
The lens geometry is defined as a TriangleMesh constructed from vertices and faces. The surface is parameterized as follows:
Base ring: A circle at the fiber tip (height = 0) forming the lens base
Concentric top rings: Each ring has an adjustable height controlled by the optimization parameters
Center vertices: Top and bottom center points to close the mesh
The height profile is smoothed using a Gaussian kernel to ensure gradual transitions between adjacent rings, which improves manufacturability and avoids sharp discontinuities that could cause meshing issues.
def vertices_from_params(params):def smooth_param_profile(params): sigma = smooth_sigma_frac *len(params)if sigma ==0:return params pad =max(1, int(4* sigma))# gaussian kernel x = anp.arange(-pad, pad +1) k = anp.exp(-0.5* (x / sigma) **2) k /= k.sum()# padding left_pad = params[1 : pad +1][::-1] right_pad = params[-pad -1 : -1][::-1] padded = anp.concatenate([left_pad, params, right_pad])# convolve smoothed = convolve(padded, k, mode="valid")return smoothed heights = smooth_param_profile(params) theta = anp.linspace(0.0, 2.0* anp.pi, ring_segments, endpoint=False) cos_t = anp.cos(theta) sin_t = anp.sin(theta) rings = []# bottom base ring at height = 0 X = anp.full(ring_segments, fiber_length) Y = base_design_radius * cos_t Z = base_design_radius * sin_t rings.append(anp.stack([X, Y, Z], axis=1))# rings for radius > 0 ring_radii = np.linspace(0.0, base_design_radius, num_rings)for radius, h inzip(ring_radii[1:], heights[1:]): X = anp.full(ring_segments, fiber_length + h) Y = radius * cos_t Z = radius * sin_t rings.append(anp.stack([X, Y, Z], axis=1)) vertices = anp.concatenate(rings, axis=0)# bottom and top center vertex top_center = anp.array([[fiber_length + heights[0], 0.0, 0.0]]) bottom_center = anp.array([[fiber_length +0.0, 0.0, 0.0]]) vertices = anp.concatenate([vertices, top_center, bottom_center], axis=0)return vertices
Next, we define the triangular faces that connect the vertices. The vertex ordering must be counter-clockwise when viewed from outside the mesh to ensure correct surface normals.
def build_faces(num_rings): faces: list[list[int]] = []def ring_offset(i: int) ->int:return i * ring_segments K = num_rings -1# index of the outermost top ring top_center_idx = num_rings * ring_segments bottom_center_idx = top_center_idx +1# ---- side wall between base ring and outermost top ring ---- base = ring_offset(0) outer = ring_offset(K)for j inrange(ring_segments): jn = (j +1) % ring_segments faces.append([base + j, base + jn, outer + j]) faces.append([base + jn, outer + jn, outer + j])# center fan on top surface: top_center ↔ ring 1 inner_ring = ring_offset(1)for j inrange(ring_segments): jn = (j +1) % ring_segments faces.append([top_center_idx, inner_ring + j, inner_ring + jn])# ring-by-ring faces between top rings:# connect 1↔2, 2↔3, ..., (K-1)↔Kfor i inrange(1, K): r0 = ring_offset(i) r1 = ring_offset(i +1)for j inrange(ring_segments): jn = (j +1) % ring_segments faces.append([r0 + j, r1 + j, r0 + jn]) faces.append([r0 + jn, r1 + j, r1 + jn])# ---- bottom disk: fan from bottom_center to base ring ---- base_start = ring_offset(0)for j inrange(ring_segments): jn = (j +1) % ring_segments faces.append([bottom_center_idx, base_start + jn, base_start + j]) faces = anp.array(faces, dtype=int)return faces
We can now construct the complete mesh from a parameter vector. The mesh_from_params function computes vertices from the parameters and combines them with the precomputed face connectivity to create a TriangleMesh object.
Before optimization, let’s verify the mesh construction by creating a lens with randomized ring heights. This helps confirm that the geometry, smoothing, and triangulation work correctly.
def get_fiber_design_sim(params, monitor_propagation=False):"""Create simulation with fiber and parameterized lens structure.""" mesh = mesh_from_params(params) base_sim = make_base_simulation(monitor_propagation=monitor_propagation) medium = td.Medium(permittivity=n_design**2) design_structure = td.Structure(geometry=mesh, medium=medium, name="lens") new_structures = [*base_sim.structures, design_structure]return base_sim.updated_copy(structures=new_structures, validate=True)# Create example with random perturbationsexample_params = np.full(num_rings, initial_height) + np.random.randn(num_rings) *0.5example_design_sim = get_fiber_design_sim(example_params)example_design_sim.plot_3d() # 3D plotexample_design_sim.plot(z=0) # 2D plot
Objective Function
The objective function defines what we want to optimize. Here, we maximize the optical power (flux) transmitted through the exit monitor at the focal plane. The function:
Constructs a simulation from the current parameters
Runs the FDTD simulation via web.run()
Extracts and returns the flux from the flux monitor
def objective(params):"""Compute the flux at the focal plane for a given set of ring heights.""" sim = get_fiber_design_sim(params) sim_data = web.run(sim, task_name="fiber_lens_opt", verbose=False) flux = sim_data["flux_exit"].flux.datareturn flux
Optimization
We use Tidy3D’s Adam optimizer to iteratively improve the lens design. The optimize does the actual iterative optimization. It requires the starting parameters (params0), the optimizer instance, the number of steps to run, optionally parameter bounds, a callback which is run on every iteration, and the optimization direction.
The gradient is computed efficiently via the adjoint method: only two simulations (forward + adjoint) are needed per step, regardless of the 128 design parameters.
Let’s analyze the optimization results by plotting the convergence history and visualizing the optimized lens alongside the field distribution. The optimized design typically exhibits a wave-like (Fresnel-like) surface profile that focuses light to the target region.
def plot_history(history):"""Plot the optimization convergence history.""" plt.figure(figsize=(8, 4)) plt.plot(np.arange(1, len(history) +1), history, "o-", linewidth=2, markersize=4) plt.xlabel("Optimization Step") plt.ylabel("Flux at Focal Plane") plt.title("Optimization Convergence") plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()def plot_design_and_field(sim, data):"""Plot the lens geometry and field distribution side by side.""" f, (ax1, ax2) = plt.subplots(1, 2, tight_layout=True, figsize=(12, 4)) sim.plot(z=0, ax=ax1) ax1.set_title("Optimized Lens (top view)") data.plot_field("field_z_0", "E", "abs^2", ax=ax2) ax2.set_title("Field Intensity |E|²") plt.show()# Plot convergenceplot_history(history["objective_fn_val"])# Run final simulation with propagation monitorfinal_sim = get_fiber_design_sim(params, monitor_propagation=True)final_data = web.run( final_sim, task_name="fiber_lens_final", verbose=False, path="fiber_lens_final.hdf5")# Report final performancefinal_flux = final_data["flux_exit"].flux.valuesprint(f"\nFinal flux at focal plane: {final_flux.item():.4e}")# Visualize resultsplot_design_and_field(final_sim, final_data)final_sim.plot_3d()
Final flux at focal plane: 3.9822e-02
Export Results
Finally, we save the optimized parameters and export the lens geometry as an STL file for fabrication or further analysis in CAD software.
# Save optimized parametersnpy_export_path ="misc/fiber_lens_params.npy"np.save(npy_export_path, get_static(params))print(f"Saved {num_rings} optimized ring heights to '{npy_export_path}'")# Export mesh as STLmesh = mesh_from_params(params)stl_export_path ="misc/fiber_lens.stl"mesh.to_stl(stl_export_path)print(f"Exported lens geometry to '{stl_export_path}'")
Saved 128 optimized ring heights to 'misc/fiber_lens_params.npy'
Exported lens geometry to 'misc/fiber_lens.stl'
Summary
In this notebook, we demonstrated inverse design of a fiber lens using Tidy3D’s native autograd support. Key takeaways:
TriangleMesh parameterization: Complex 3D surfaces can be optimized by making vertex positions differentiable
Gaussian smoothing: Applying a smoothing kernel to parameters ensures manufacturable designs
Adjoint efficiency: Gradients for 128 parameters are computed with just 2 simulations per step
STL export: Optimized geometries can be directly exported for fabrication
We use necessary cookies to run this website. With your permission, we
also use analytics cookies to understand site usage and marketing
cookies for advertising, retargeting, and HubSpot tracking.
Learn more about our cookie policy.
Privacy choices
Choose which optional cookies Flexcompute may use. Necessary cookies
are always on because they support core website behavior, security, and
saving your consent record.
Your browser is sending a Global Privacy Control signal, so marketing
cookies are disabled.
Subscribe
Thanks for subscribing
Publish Your Notebook
Thank you for publishingA confirmation email has been sent to your inbox. Your notebook will be available within the next 48 hours.
Community Library
TERMS & CONDITIONS
EFFECTIVE DATE: January 1, 2025
Terms and Conditions for User-Submitted Content
By submitting content to Flexcompute, you agree to the following terms:
1. Ownership and Copyright
All content, including but not limited to text, data, images, and other materials submitted by users,
remains the sole property of the original creator. Flexcompute does not claim ownership of the submitted
content or any intellectual property rights associated with it.
Users affirm that they own the copyright or have obtained all necessary permissions for the submitted
content and are fully responsible for ensuring that their submissions do not infringe on any third-party
rights.
2. Responsibility for Content
Users are solely responsible for the content they submit. Flexcompute does not endorse, guarantee,
or verify the accuracy, legality, or appropriateness of any submitted content. Users are responsible
for ensuring that their content complies with all applicable laws and regulations.
Users declare that they have obtained authorization from all co-authors and collaborators associated
with the submitted content, granting them the right to submit and sign on behalf of all contributors.
Users agree not to submit content that is illegal, defamatory, obscene, or violates the rights
of others, including privacy and intellectual property rights.
3. Modification and Presentation
Flexcompute reserves the right to review, edit, or modify submitted content to improve clarity,
formatting, and presentation while maintaining the original intent and message.
These modifications are made to enhance the overall quality and readability of the published material.
Users acknowledge that Flexcompute may format or present the content in a way
that aligns with our editorial and visual standards.
4. Liability Disclaimer
Flexcompute shall not be held liable for any disputes arising from user-submitted content, including
but not limited to copyright claims, inaccuracies, or damages resulting from the publication of user content.
Users agree to indemnify Flexcompute against any claims or legal actions resulting from their submitted content.
5. Content Use Rights
By submitting content, users grant Flexcompute a non-exclusive, royalty-free, worldwide license to publish, distribute,
and promote the content for the purposes of showcasing user contributions and marketing our services.
This license does not transfer ownership of the content or any copyright to Flexcompute.
Users retain the right to withdraw their submissions at any time.
Upon request, Flexcompute will remove the content from its platform.
6. Privacy and Confidentiality
Users acknowledge that submitted content may be publicly accessible and therefore waive any
rights to confidentiality or privacy regarding the published material.
Flexcompute will not share personal information of users without
their consent, in accordance with our privacy policy.
Community Library
How the process works:
You submit your notebook.
You will get a permanent URL to promote your work.
Cite the URL as a reference for others in your upcoming papers to enhance the impact.
When someone clicks your URL, your notebook will be displayed.
Enter your email address below to receive the presentation slides. In the future, we’ll share you very few emails when we have new tutorial release, development updates, valuable toolkits and technical guidance . You can unsubscribe at any time by clicking the link at the bottom of every email. We’ll never share your information.