@@ -81,27 +81,33 @@ def psf(self, points, wvln=None, ks=PSF_KS, **kwargs):
8181 recenter (bool): If True (default), center the PSF on the chief
8282 ray; otherwise on the pinhole projection.
8383 model (str): One of 'geometric' (default), 'coherent', 'huygens'.
84+ return_field (bool): For the coherent and Huygens models, return
85+ the energy-normalized complex sensor-plane field instead of
86+ intensity. Defaults to False.
8487
8588 Returns:
86- psf (torch.Tensor): PSF normalized to sum to 1. Shape [ks, ks] for a
87- single point, or [N, ks, ks] for the geometric model with N points.
89+ torch.Tensor: Intensity PSF normalized to sum to 1, or an
90+ energy-normalized complex field when ``return_field=True``.
91+ Shape [ks, ks] for a single point, or [N, ks, ks] for the
92+ geometric model with N points.
8893
8994 Raises:
9095 ValueError: If `model` is not one of the supported names.
9196 """
9297 wvln = self .primary_wvln if wvln is None else wvln
9398 spp = kwargs .get ("spp" , None )
9499 recenter = kwargs .get ("recenter" , True )
100+ return_field = kwargs .get ("return_field" , False )
95101 model = kwargs .get ("model" , "geometric" )
96102 if model == "geometric" :
97103 spp = SPP_PSF if spp is None else spp
98104 return self .psf_geometric (points , ks , wvln , spp , recenter )
99105 elif model == "coherent" :
100106 spp = SPP_COHERENT if spp is None else spp
101- return self .psf_coherent (points , ks , wvln , spp , recenter )
107+ return self .psf_coherent (points , ks , wvln , spp , recenter , return_field )
102108 elif model == "huygens" :
103109 spp = SPP_COHERENT if spp is None else spp
104- return self .psf_huygens (points , ks , wvln , spp , recenter )
110+ return self .psf_huygens (points , ks , wvln , spp , recenter , return_field )
105111 else :
106112 raise ValueError (f"Unknown PSF model: { model } " )
107113
@@ -182,7 +188,13 @@ def psf_geometric(self, points, ks=PSF_KS, wvln=None, spp=SPP_PSF, recenter=True
182188 return diff_float (psf )
183189
184190 def psf_coherent (
185- self , points , ks = PSF_KS , wvln = None , spp = SPP_COHERENT , recenter = True
191+ self ,
192+ points ,
193+ ks = PSF_KS ,
194+ wvln = None ,
195+ spp = SPP_COHERENT ,
196+ recenter = True ,
197+ return_field = False ,
186198 ):
187199 """Compute the coherent exit-pupil PSF (alias for `psf_pupil_prop`).
188200
@@ -198,22 +210,43 @@ def psf_coherent(
198210 back to `self.primary_wvln`.
199211 spp (int, optional): Rays sampled. Defaults to SPP_COHERENT.
200212 recenter (bool, optional): If True (default), center on the chief ray.
213+ return_field (bool, optional): Return the energy-normalized complex
214+ sensor-plane field instead of intensity. Defaults to False.
201215
202216 Returns:
203- psf (torch.Tensor): PSF normalized to sum to 1. Shape [ks, ks].
217+ torch.Tensor: Intensity PSF normalized to sum to 1, or an
218+ energy-normalized complex field when ``return_field=True``.
219+ Shape [ks, ks].
220+
221+ Note:
222+ The complex field has an arbitrary global phase because optical path
223+ length is measured relative to a reference; relative phase is meaningful.
204224 """
205225 wvln = self .primary_wvln if wvln is None else wvln
206- return self .psf_pupil_prop (points , ks = ks , wvln = wvln , spp = spp , recenter = recenter )
226+ return self .psf_pupil_prop (
227+ points ,
228+ ks = ks ,
229+ wvln = wvln ,
230+ spp = spp ,
231+ recenter = recenter ,
232+ return_field = return_field ,
233+ )
207234
208235 def psf_pupil_prop (
209- self , points , ks = PSF_KS , wvln = None , spp = SPP_COHERENT , recenter = True
236+ self ,
237+ points ,
238+ ks = PSF_KS ,
239+ wvln = None ,
240+ spp = SPP_COHERENT ,
241+ recenter = True ,
242+ return_field = False ,
210243 ):
211244 """Compute the single-point monochromatic PSF via the exit-pupil diffraction model.
212245
213246 Steps:
214247 1. Compute the complex wavefront at the exit-pupil plane by coherent ray tracing.
215248 2. Propagate to the sensor plane with the Angular Spectrum Method (ASM)
216- and take the intensity as the PSF . This function is differentiable.
249+ and return either its field or intensity . This function is differentiable.
217250
218251 Args:
219252 points (torch.Tensor or list): Single normalized point source [3] or
@@ -226,12 +259,14 @@ def psf_pupil_prop(
226259 spp (int, optional): Number of rays to sample. Defaults to SPP_COHERENT.
227260 recenter (bool, optional): If True (default), center on the chief ray;
228261 otherwise on the pinhole projection.
262+ return_field (bool, optional): Return the energy-normalized complex
263+ sensor-plane field instead of intensity. Defaults to False.
229264
230265 Returns:
231- psf ( torch.Tensor): PSF normalized to sum to 1. Shape [ks, ks] when
232- `ks` is given, or the full uncropped intensity field of shape
233- [ 1, 1, 2H, 2H] (twice the exit-pupil grid after zero-padding)
234- when `ks` is None.
266+ torch.Tensor: Intensity PSF normalized to sum to 1, or an
267+ energy-normalized complex field when ``return_field=True``.
268+ Shape [ks, ks] when `ks` is given, or [ 1, 1, 2H, 2H] when
269+ `ks` is None (twice the exit-pupil grid after zero-padding) .
235270
236271 Reference:
237272 [1] "End-to-End Hybrid Refractive-Diffractive Lens Design with Differentiable Ray-Wave Model", SIGGRAPH Asia 2024.
@@ -241,6 +276,8 @@ def psf_pupil_prop(
241276 Angular Spectrum Method (ASM) instead of a single FFT. ASM is more
242277 accurate because the FFT approach assumes a far-field condition
243278 (e.g., chief ray perpendicular to the image plane).
279+ The complex field has an arbitrary global phase because optical path
280+ length is measured relative to a reference; relative phase is meaningful.
244281 """
245282 wvln = self .primary_wvln if wvln is None else wvln
246283 # Pupil field by coherent ray tracing
@@ -275,6 +312,26 @@ def psf_pupil_prop(
275312 psfc_idx_i = ((2 - psfc [1 ]) * h / 4 ).round ().long ()
276313 psfc_idx_j = ((2 + psfc [0 ]) * w / 4 ).round ().long ()
277314
315+ if return_field :
316+ field = sensor_field
317+ if ks is not None :
318+ field = (
319+ F .pad (
320+ field ,
321+ [ks // 2 , ks // 2 , ks // 2 , ks // 2 ],
322+ mode = "constant" ,
323+ value = 0 ,
324+ )
325+ .squeeze (0 )
326+ .squeeze (0 )
327+ )
328+ field = field [
329+ psfc_idx_i : psfc_idx_i + ks ,
330+ psfc_idx_j : psfc_idx_j + ks ,
331+ ]
332+ energy = field .abs ().square ().sum (dim = (- 2 , - 1 ), keepdim = True )
333+ return field / torch .sqrt (energy + EPSILON )
334+
278335 # Crop valid PSF region and normalize
279336 if ks is not None :
280337 psf_inten_pad = (
@@ -392,7 +449,13 @@ def pupil_field(self, points, wvln=None, spp=SPP_COHERENT, recenter=True):
392449 return wavefront , psf_center
393450
394451 def psf_huygens (
395- self , points , ks = PSF_KS , wvln = None , spp = SPP_COHERENT , recenter = True
452+ self ,
453+ points ,
454+ ks = PSF_KS ,
455+ wvln = None ,
456+ spp = SPP_COHERENT ,
457+ recenter = True ,
458+ return_field = False ,
396459 ):
397460 """Compute the single-wavelength Huygens PSF by spherical-wave integration.
398461
@@ -414,16 +477,22 @@ def psf_huygens(
414477 spp (int, optional): Rays sampled. Defaults to SPP_COHERENT.
415478 recenter (bool, optional): If True (default), center on the chief ray;
416479 otherwise on the pinhole projection.
480+ return_field (bool, optional): Return the energy-normalized complex
481+ sensor-plane field instead of intensity. Defaults to False.
417482
418483 Returns:
419- psf (torch.Tensor): PSF normalized to sum to 1. Shape [ks, ks].
484+ torch.Tensor: Intensity PSF normalized to sum to 1, or an
485+ energy-normalized complex field when ``return_field=True``.
486+ Shape [ks, ks].
420487
421488 Reference:
422489 [1] "Optical Aberrations Correction in Postprocessing Using Imaging Simulation", TOG 2021.
423490
424491 Note:
425492 Different from the ZEMAX Huygens PSF, which traces rays to the image
426493 plane and performs plane-wave integration.
494+ The complex field has an arbitrary global phase because optical path
495+ length is measured relative to a reference; relative phase is meaningful.
427496 """
428497 wvln = self .primary_wvln if wvln is None else wvln
429498 if self .dtype != torch .float64 :
@@ -541,6 +610,11 @@ def psf_huygens(
541610 # Sum contributions from this batch
542611 psf_complex += complex_amp .sum (dim = - 1 ) # [ks, ks]
543612
613+ if return_field :
614+ field = torch .flip (psf_complex , [- 2 , - 1 ])
615+ energy = field .abs ().square ().sum (dim = (- 2 , - 1 ), keepdim = True )
616+ return field / torch .sqrt (energy + EPSILON )
617+
544618 # Convert complex field to intensity
545619 psf = psf_complex .abs () ** 2
546620
0 commit comments