Skip to content

Commit 91c1d25

Browse files
Feat/extended sources (#470)
* feat: implement extended source modelling logic and visualization (#224) * feat: improve source visualization in optical system and source.draw() method (#224) * correct formulas for creating the collimated gaussian source, and add a notebook as an example - to me moved to the gallery (#224) * formatting * refactor source logic: Add SMF Source model, ExtendedSourceOptic wrapper, and a correction term added for radiant intensity computation (#224) * ruff check * docs: update documentation to include ExtendedSource approach. Includes an example Notebook Tutorial and for the gallery as well * Add comprehensive tests for extended sources and fix torch XY projection - Add tests/test_extended_sources.py with 160 tests covering: - SMFSource: ray generation, parameter computation, edge cases, visualization - BaseSource: abstract interface contract verification - ExtendedSourceOptic: delegation, tracing, drawing, projections, repr - IncoherentIrradiance: source integration and conflict handling - SourceViewer: 6-panel diagnostic visualization - Standard analysis delegation via .optic for field-based analyses - Fix torch backend compatibility in Lens2D XY projection (optiland/visualization/system/lens.py): convert tensor values to plain floats via be.to_numpy().item() before passing to plt.Circle, preventing RuntimeError from matplotlib calling .numpy() on tensors with requires_grad=True * chore: trying to resolve doc gen failure. Remove autogen'd files * chore: alphabetize api * chore: run notebook * docs: add extended sources to MD learning guide --------- Co-authored-by: Kramer <kdanielharrison@gmail.com>
1 parent a431fac commit 91c1d25

25 files changed

+2685
-129
lines changed

docs/LEARNING_GUIDE.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,17 @@ Welcome to the Optiland Learning Guide! This guide walks you through key concept
9999
- Adding new coating types
100100
- [Tutorial 10c - Custom Optimization Algorithms](https://github.com/HarrisonKramer/optiland/blob/master/docs/examples/Tutorial_10c_Custom_Optimization_Algorithm.ipynb)
101101
- Creating a "random walk optimizer" to optimize an aspheric singlet
102-
11. **Machine Learning in Optical Design** - note that these notebooks are hosted in the [LensAI repository](https://github.com/HarrisonKramer/LensAI)
103-
- [Tutorial 11a - Random Forest Regressor to Predict Optimal Lens Properties](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_1/Singlet_RF_Model_RMS_Spot_Size.ipynb)
102+
11. **Extended Sources**
103+
- [Tutorial 11a - Extended Source Modeling](https://github.com/HarrisonKramer/optiland/blob/master/docs/examples/Tutorial_11a_Extended_Source_Modeling.ipynb)
104+
- Modeling extended sources
105+
12. **Machine Learning in Optical Design** - note that these notebooks are hosted in the [LensAI repository](https://github.com/HarrisonKramer/LensAI)
106+
- [Tutorial 12a - Random Forest Regressor to Predict Optimal Lens Properties](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_1/Singlet_RF_Model_RMS_Spot_Size.ipynb)
104107
- Demonstrates how to build and train a random forest regressor to predict the radius of curvature of a plano-convex lens in order to minimize the RMS spot size.
105-
- [Tutorial 11b - Ray Path Failure Classification Model](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_2/Ray_Path_Failure_Classification_Model.ipynb)
108+
- [Tutorial 12b - Ray Path Failure Classification Model](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_2/Ray_Path_Failure_Classification_Model.ipynb)
106109
- Uses logistic regression to predict ray path failures in a Cooke triplet design.
107-
- [Tutorial 11c - Surrogate Ray Tracing Model Using Neural Networks](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_3/Double_Gauss_Surrogate_Model.ipynb)
110+
- [Tutorial 12c - Surrogate Ray Tracing Model Using Neural Networks](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_3/Double_Gauss_Surrogate_Model.ipynb)
108111
- Builds a neural network surrogate ray tracing model to increase effective "ray tracing" speed by 10,000x.
109-
- [Tutorial 11d - Super-Resolution Generative Adversarial Network to Enhance Wavefront Map Data](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_5/SR_GAN_for_wavefront_data.ipynb)
112+
- [Tutorial 12d - Super-Resolution Generative Adversarial Network to Enhance Wavefront Map Data](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_5/SR_GAN_for_wavefront_data.ipynb)
110113
- Utilizes a super-resolution GAN (SRGAN) to upscale low-resolution wavefront data into high-resolution data.
111-
- [Tutorial 11e - Optimization of Aspheric Lenses via Reinforcement Learning](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_4/RL_aspheric_singlet.ipynb)
114+
- [Tutorial 12e - Optimization of Aspheric Lenses via Reinforcement Learning](https://github.com/HarrisonKramer/LensAI/blob/main/notebooks/Example_4/RL_aspheric_singlet.ipynb)
112115
- Reinforcement learning is applied to the optimization of aspheric singlet lenses to generate new lens designs.

docs/api/api_optic.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ Optic
33

44
This section includes the core optic module and the optic updater module. The Optic class is the primary container for an optical system in Optiland.
55
The OpticUpdater class is used to update various properties of an Optic object, such as surface properties or polarization.
6+
The ExtendedSourceOptic class is a wrapper around the Optic class that enables extended source ray tracing.
67

78
.. autosummary::
89
:toctree: optic/
910
:caption: Optic Modules
1011

1112
optic.optic
1213
optic.optic_updater
14+
optic.extended_source_optic

docs/api/api_sources.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Sources
2+
=======
3+
4+
This section covers the extended source modeling modules of the Optiland package.
5+
Extended sources allow users to define spatially and angularly extended light sources
6+
(such as single-mode fiber outputs) and trace them through optical systems.
7+
8+
The :class:`sources.base.BaseSource` class defines the abstract interface that all
9+
source implementations must follow. Concrete implementations, such as
10+
:class:`sources.smf.SMFSource`, provide specific source types with their own
11+
spatial and angular distributions.
12+
13+
.. autosummary::
14+
:toctree: sources/
15+
:caption: Sources Modules
16+
17+
sources.base
18+
sources.smf
19+
sources.visualization

docs/examples/Tutorial_11a_Extended_Source_Modeling.ipynb

Lines changed: 273 additions & 0 deletions
Large diffs are not rendered by default.

docs/functionalities.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ Optimization and Tolerancing
4949
- **Extensible Framework**:
5050
Add new optimization variables, constraints, or algorithms with minimal overhead.
5151

52+
Extended Source Modeling
53+
-----------------------
54+
55+
- **Extended Source Ray Tracing**:
56+
Model spatially and angularly extended light sources and trace them through optical systems using the ``ExtendedSourceOptic`` wrapper.
57+
- **Single-Mode Fiber (SMF) Source**:
58+
Generate physically accurate ray bundles representing fiber outputs, with Gaussian spatial and angular distributions and quasi-random Sobol sampling.
59+
- **Custom Source Support**:
60+
Define custom source types by inheriting from ``BaseSource`` and implementing the ``generate_rays`` method.
61+
- **Source Visualization**:
62+
Validate source definitions with multi-panel diagnostic plots showing spatial distributions, angular distributions, cross-sections, and ray propagation paths.
63+
- **Irradiance Analysis Compatibility**:
64+
Compute incoherent irradiance distributions at detector surfaces from extended source illumination.
65+
5266
Material Database
5367
-----------------
5468

docs/gallery/extended_sources.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.. _gallery_extended_sources:
2+
3+
Extended Sources
4+
================
5+
6+
This section contains examples of extended source modeling in **Optiland**. Extended sources allow
7+
users to define spatially and angularly extended light sources and trace them through optical systems,
8+
enabling physically accurate simulations of fiber-coupled and other non-point-source illumination.
9+
10+
.. nbgallery::
11+
12+
extended_sources/beam_shaping_singlet

docs/gallery/extended_sources/beam_shaping_singlet.ipynb

Lines changed: 234 additions & 0 deletions
Large diffs are not rendered by default.

docs/index.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ Not sure what to type in the shell? Here are a few ideas to explore Optiland rig
108108
gallery/differentiable_ray_tracing
109109
gallery/real_world_projects
110110
gallery/external_tools
111+
gallery/extended_sources
111112
gallery/miscellaneous
112113

113114

@@ -175,6 +176,7 @@ Not sure what to type in the shell? Here are a few ideas to explore Optiland rig
175176
api/api_rays
176177
api/api_raytrace
177178
api/api_solves
179+
api/api_sources
178180
api/api_surfaces
179181
api/api_tolerancing
180182
api/api_visualization

docs/learning_guide.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,15 @@ This learning guide breaks down Optiland into a series of tutorials that cover t
114114
examples/Tutorial_10b_Custom_Coating_Types
115115
examples/Tutorial_10c_Custom_Optimization_Algorithm
116116

117-
11. Machine Learning in Optical Design
117+
11. Extended Source Modeling
118+
---------------------------
119+
120+
.. toctree::
121+
:maxdepth: 1
122+
123+
examples/Tutorial_11a_Extended_Source_Modeling
124+
125+
12. Machine Learning in Optical Design
118126
--------------------------------------
119127

120128
These examples demonstrate how Optiland can be used in conjunction with machine learning to solve optical design problems.

optiland/analysis/intensity.py

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ class RadiantIntensity(BaseAnalysis):
4848
num_rays (int): Number of rays to trace if user_initial_rays is None.
4949
distribution_name (str): Ray distribution if user_initial_rays is None.
5050
user_initial_rays (RealRays | None): Optional user-provided initial rays.
51+
source (BaseSource | None): Optional extended source object
52+
(e.g., GaussianSource) to generate initial rays automatically.
53+
Cannot be used with user_initial_rays. When provided, num_rays
54+
determines how many rays to generate.
5155
data (list[list[tuple]]): Stores (intensity_map,
5256
angle_X_bin_edges, angle_Y_bin_edges,
5357
angle_X_bin_centers, angle_Y_bin_centers)
@@ -73,6 +77,8 @@ def __init__(
7377
num_rays: int = 100000,
7478
distribution: DistributionType = "random",
7579
user_initial_rays=None,
80+
source=None,
81+
skip_trace: bool = False,
7682
):
7783
if fields == "all":
7884
self.fields = optic.fields.get_field_coords()
@@ -81,6 +87,31 @@ def __init__(
8187
fields = [fields]
8288
self.fields = tuple(fields)
8389

90+
# Handle source integration
91+
if source is not None and user_initial_rays is not None:
92+
raise ValueError("Cannot specify both 'source' and 'user_initial_rays'.")
93+
94+
self.user_initial_rays = user_initial_rays
95+
if source is not None:
96+
# Generate rays from the extended source
97+
self.user_initial_rays = source.generate_rays(num_rays)
98+
# When using a source, we treat all rays as a single "field"
99+
# The source emission defines the field, not optic.fields
100+
self.fields = [(0.0, 0.0)] # Single dummy field for source rays
101+
102+
self._initial_ray_data = None
103+
if self.user_initial_rays is not None:
104+
self._initial_ray_data = {
105+
"x": self.user_initial_rays.x,
106+
"y": self.user_initial_rays.y,
107+
"z": self.user_initial_rays.z,
108+
"L": self.user_initial_rays.L,
109+
"M": self.user_initial_rays.M,
110+
"N": self.user_initial_rays.N,
111+
"intensity": self.user_initial_rays.i,
112+
"wavelength": self.user_initial_rays.w,
113+
}
114+
84115
self.num_angular_bins_X = num_angular_bins_X
85116
self.num_angular_bins_Y = num_angular_bins_Y
86117
self.angle_X_min, self.angle_X_max = float(angle_X_min), float(angle_X_max)
@@ -89,7 +120,7 @@ def __init__(
89120
# for absolute units, we need to ensure the user has provided rays
90121
# with 'calibrated' power
91122
self.use_absolute_units = use_absolute_units
92-
if self.use_absolute_units and user_initial_rays is None:
123+
if self.use_absolute_units and self.user_initial_rays is None:
93124
print(
94125
"Warning: `use_absolute_units` is True, but no `user_initial_rays` "
95126
"were provided."
@@ -103,7 +134,7 @@ def __init__(
103134
self.reference_surface_index = int(reference_surface_index)
104135
self.num_rays = num_rays
105136
self.distribution_name: DistributionType = distribution
106-
self.user_initial_rays = user_initial_rays
137+
self.skip_trace = skip_trace
107138

108139
super().__init__(optic, wavelengths)
109140

@@ -121,15 +152,19 @@ def _generate_data(self):
121152
def _generate_field_wavelength_data(
122153
self, field_coord: tuple[ScalarOrArray, ScalarOrArray], wavelength: float
123154
) -> tuple[BEArray, BEArray, BEArray, BEArray, BEArray]:
124-
if self.user_initial_rays is None:
125-
self.optic.trace(
126-
*field_coord,
127-
wavelength=wavelength,
128-
num_rays=self.num_rays,
129-
distribution=self.distribution_name,
130-
)
131-
else:
132-
self.optic.surface_group.trace(self.user_initial_rays)
155+
if not self.skip_trace:
156+
if self.user_initial_rays is None:
157+
self.optic.trace(
158+
*field_coord,
159+
wavelength=wavelength,
160+
num_rays=self.num_rays,
161+
distribution=self.distribution_name,
162+
)
163+
else:
164+
from optiland.rays import RealRays
165+
166+
rays_to_trace = RealRays(**self._initial_ray_data)
167+
self.optic.surface_group.trace(rays_to_trace)
133168

134169
surf_group = self.optic.surface_group
135170
try:
@@ -200,13 +235,43 @@ def _generate_field_wavelength_data(
200235
)
201236

202237
if self.use_absolute_units:
203-
delta_angle_X_rad = be.radians(angle_X_bins[1] - angle_X_bins[0])
204-
delta_angle_Y_rad = be.radians(angle_Y_bins[1] - angle_Y_bins[0])
205-
solid_angle_per_bin_sr = delta_angle_X_rad * delta_angle_Y_rad
206-
238+
# 1. Calculate basic bin sizes (radians)
239+
dx = be.radians(angle_X_bins[1] - angle_X_bins[0])
240+
dy = be.radians(angle_Y_bins[1] - angle_Y_bins[0])
241+
242+
# 2. Create meshgrid of bin centers (in Radians) for
243+
# the Jacobian calculation
244+
# Note: We must be careful with tensor shapes here to match
245+
# power_map (Y, X)
246+
ax_c_rad = be.radians(angle_X_centers)
247+
ay_c_rad = be.radians(angle_Y_centers)
248+
249+
# Create grids. Meshgrid usually returns (Y, X) with
250+
# indexing='ij' or 'xy' depending on backend
251+
# For safety, let's explicitely broadcast
252+
# resulting shape (Y, X) usually
253+
AX, AY = be.meshgrid(ax_c_rad, ay_c_rad)
254+
255+
# 3. Compute Jacobian terms
256+
# J = (sec^2(tx) * sec^2(ty)) / (1 + tan^2(tx) + tan^2(ty))^(3/2)
257+
tan2_tx = be.tan(AX) ** 2
258+
tan2_ty = be.tan(AY) ** 2
259+
sec2_tx = 1.0 + tan2_tx # Identity: sec^2 = 1 + tan^2
260+
sec2_ty = 1.0 + tan2_ty
261+
262+
numerator = sec2_tx * sec2_ty
263+
denominator = (1.0 + tan2_tx + tan2_ty) ** 1.5
264+
265+
jacobian_factor = numerator / denominator
266+
267+
# 4. Compute true solid angle per bin
268+
# d_omega = J * d_theta_x * d_theta_y
269+
true_solid_angle_map = jacobian_factor * dx * dy
270+
271+
# 5. Normalize Power Map
207272
final_intensity_map = be.where(
208-
solid_angle_per_bin_sr > 1e-12,
209-
power_map / solid_angle_per_bin_sr,
273+
true_solid_angle_map > 1e-12,
274+
power_map / true_solid_angle_map,
210275
be.zeros_like(power_map),
211276
)
212277
else:

0 commit comments

Comments
 (0)