1+ """
2+ Provides a phase profile based on a height map and dispersive material.
3+ """
4+
5+ from __future__ import annotations
6+
7+ from optiland import backend as be
8+ from optiland .materials .base import BaseMaterial
9+ from optiland .phase .base import BasePhaseProfile
10+
11+ try :
12+ from scipy .interpolate import RectBivariateSpline
13+ except ImportError :
14+ RectBivariateSpline = None
15+
16+
17+ class HeightProfile (BasePhaseProfile ):
18+ """A phase profile defined by a height map and a dispersive material.
19+
20+ The phase is calculated as:
21+ phi(x, y, λ) = (2π / λ) * (n_material(λ) - 1) * h(x, y)
22+
23+ Assumes air as the reference medium.
24+
25+ Args:
26+ x_coords (be.Array): X-coordinates of the height map grid.
27+ y_coords (be.Array): Y-coordinates of the height map grid.
28+ height_map (be.Array): Height values at grid points
29+ with shape (len(y_coords), len(x_coords)).
30+ material: Material providing wavelength-dependent refractive index n(λ).
31+ """
32+
33+ phase_type = "height_profile"
34+
35+ def __init__ (
36+ self ,
37+ x_coords : be .Array ,
38+ y_coords : be .Array ,
39+ height_map : be .Array ,
40+ material : BaseMaterial ,
41+ ):
42+ if RectBivariateSpline is None :
43+ raise ImportError (
44+ "scipy is required for HeightProfile. Install with: pip install scipy"
45+ )
46+
47+ self .x_coords = be .to_numpy (x_coords )
48+ self .y_coords = be .to_numpy (y_coords )
49+ self .height_map = be .to_numpy (height_map )
50+ self .material = material
51+
52+ self ._spline = RectBivariateSpline (
53+ self .y_coords ,
54+ self .x_coords ,
55+ self .height_map ,
56+ )
57+
58+ def _interpolate_height (self , x : be .Array , y : be .Array ) -> be .Array :
59+ return self ._spline .ev (be .to_numpy (y ), be .to_numpy (x ))
60+
61+ def _interpolate_gradient (
62+ self , x : be .Array , y : be .Array
63+ ) -> tuple [be .Array , be .Array ]:
64+ x_np = be .to_numpy (x )
65+ y_np = be .to_numpy (y )
66+ dh_dx = self ._spline .ev (y_np , x_np , dy = 1 )
67+ dh_dy = self ._spline .ev (y_np , x_np , dx = 1 )
68+ return dh_dx , dh_dy
69+
70+ def get_phase (
71+ self ,
72+ x : be .Array ,
73+ y : be .Array ,
74+ wavelength : be .Array ,
75+ ) -> be .Array :
76+ h = self ._interpolate_height (x , y )
77+ n = self .material .n (wavelength )
78+ return 2 * be .pi / (wavelength * 1e-3 ) * (n - 1.0 ) * h
79+
80+ def get_gradient (
81+ self ,
82+ x : be .Array ,
83+ y : be .Array ,
84+ wavelength : be .Array ,
85+ ) -> tuple [be .Array , be .Array , be .Array ]:
86+ dh_dx , dh_dy = self ._interpolate_gradient (x , y )
87+ n = self .material .n (wavelength )
88+ factor = 2 * be .pi / (wavelength * 1e-3 ) * (n - 1.0 )
89+ return factor * dh_dx , factor * dh_dy , be .zeros_like (x )
90+
91+ def get_paraxial_gradient (
92+ self ,
93+ y : be .Array ,
94+ wavelength : be .Array ,
95+ ) -> be .Array :
96+ dh_dy = self ._spline .ev (
97+ be .to_numpy (y ),
98+ be .zeros_like (y ),
99+ dx = 1 ,
100+ )
101+ n = self .material .n (wavelength )
102+ return 2 * be .pi / (wavelength * 1e-3 ) * (n - 1.0 ) * dh_dy
103+
104+ def to_dict (self ) -> dict :
105+ data = super ().to_dict ()
106+ data ["x_coords" ] = self .x_coords .tolist ()
107+ data ["y_coords" ] = self .y_coords .tolist ()
108+ data ["height_map" ] = self .height_map .tolist ()
109+ data ["material" ] = getattr (self .material , "name" , str (self .material ))
110+ return data
111+
112+ @classmethod
113+ def from_dict (cls , data : dict ) -> HeightProfile :
114+ return cls (
115+ x_coords = be .array (data ["x_coords" ]),
116+ y_coords = be .array (data ["y_coords" ]),
117+ height_map = be .array (data ["height_map" ]),
118+ material = data ["material" ],
119+ )
0 commit comments