2025, Oct 20 07:33
Angular spectrum में Gaussian beam: सही propagation step और dz क्यों मायने रखते हैं
FFT-आधारित angular spectrum propagation में लंबी दूरी पर Gaussian beam के विचलन का मुख्य कारण बहुत छोटा dz है. सही step size और त्रुटि घटाने की विधि जानें.
FFT-आधारित angular spectrum propagation दूरी के साथ पार्श्व ऑप्टिकल फील्ड को आगे बढ़ाने का भरोसेमंद तरीका है, लेकिन जब ग्रिड, किनारे और स्टेप साइज़ एक-दूसरे से टकराते हैं, तो दिक्कतें आ सकती हैं। आम समाधान है अवशोषक सीमाएँ जोड़ना और प्रसार को छोटे-छोटे चरणों में बाँटना, ताकि फील्ड बॉक्स के किनारों तक न पहुँचे। अगर फिर लंबी दूरी पर ही प्रोफ़ाइल विश्लेषणात्मक हल से भटकने लगे, तो शक आमतौर पर फ़िल्टर पर जाता है। मगर नीचे के मामले में फ़िल्टर ठीक है। अस्थिरता की जड़ बहुत छोटा propagation step है, जो अत्यधिक FFT/ifft चक्र करवाता है और संख्यात्मक त्रुटि जमा करता है।
समस्या का सेटअप
लक्ष्य है z = z0 पर सैंपल किए गए Gaussian फील्ड से शुरू करना, उसे दूरी d तक प्रचारित करना, और z = z0 + d पर प्राप्त संख्यात्मक परिणाम की तुलना उसी तल पर मौजूद विश्लेषणात्मक Gaussian बीम से करना। ऊर्जा को किनारों से दूर रखने के लिए absorbing boundary मास्क के साथ चरणबद्ध (stepwise) propagation अपनाया गया है। अपेक्षाकृत छोटी दूरी पर सब कुछ अच्छी तरह मेल खाता है। लंबी दूरी पर, जैसे ही बीम किनारों के पास आता है, स्मूद मास्क होने पर भी संख्यात्मक परिणाम विश्लेषणात्मक बीम से अलग होने लगता है।
असंगति को पुन: उत्पन्न करने वाला कोड उदाहरण
नीचे दिया गया स्निपेट मुख्य तर्क दिखाता है: Gaussian बीम की परिभाषा, स्थानिक-आवृत्ति डोमेन में एकल-चरण propagator, अवशोषक किनारे के साथ मल्टी-स्टेप ड्राइवर, और लक्ष्य तल पर विश्लेषणात्मक फील्ड से तुलना करने वाला परीक्षण। वेरिएबल नाम अंग्रेज़ी में हैं और तर्क ऊपर बताए सेटअप से मेल खाता है।
import numpy as np
from numpy.fft import fft2, ifft2, fftfreq
from numpy import exp, pi, sqrt
arctg = np.arctan2
class BeamCore:
  def __init__(self, zR: float, lambda0: float):
    self.zR = zR
    self.lambda0 = lambda0
    self.k = 2 * pi / self.lambda0
    self.w0 = sqrt(self.lambda0 * self.zR / pi)
    self.theta0 = self.w0 / self.zR
    self.a0 = sqrt(2 / (pi * self.w0))
    self.i0 = self.a0 * self.a0.conjugate()
  def w(self, z: np.array):
    return self.w0 * sqrt(1 + (z / self.zR)**2)
  def R(self, z: np.array):
    z_safe = np.where(z == 0.0, 1e-20, z)
    rval = z_safe * (1 + (self.zR / z_safe)**2)
    rval = np.where(z == 0.0, np.inf, rval)
    return rval
  def gouy(self, z: np.array):
    return arctg(z, self.zR)
class GaussianBeam(BeamCore):
  def __init__(self, zR: float, lambda0: float):
    super().__init__(zR, lambda0)
  def field(self, z: np.array = 0.0, r: np.array = 0.0, phi: np.array = 0.0) -> np.array:
    z = np.asarray(z, dtype=float)
    w = self.w(z)
    R = self.R(z)
    zeta = self.gouy(z)
    U = self.a0 * (self.w0 / w) * exp(-r**2 / w**2 + (-self.k * z - self.k * (r**2) / (2 * R) + zeta) * 1.0j)
    return U
def step_propagate(U, dz, kz):
  UF = fft2(U)
  if dz > 0.0:
    UFprop = UF * exp(1j * kz * dz).conj()
  elif dz < 0.0:
    UFprop = UF * exp(1j * kz * (-dz))
  Uout = ifft2(UFprop)
  return Uout
def propagate_sliced(U, zdist, x1d, y1d, k0, edge_mask):
  nx, ny = U.shape
  dx = (x1d[-1] - x1d[0]) / (nx - 1)
  dy = (y1d[-1] - y1d[0]) / (ny - 1)
  fx = fftfreq(nx, d=dx)
  fy = fftfreq(ny, d=dy)
  FX, FY = np.meshgrid(fx, fy)
  kx = 2 * pi * FX
  ky = 2 * pi * FY
  kz = sqrt(k0**2 - kx**2 - ky**2 + 0j)
  Ustep = U
  while zdist != 0.0:
    U_faded = Ustep * edge_mask
    dz = 0.00005  # बहुत छोटा स्टेप
    dz = min(dz, zdist)
    Ustep = step_propagate(U_faded, dz, kz)
    zdist -= dz
  return Ustep
# पैरामीटर
lambda0 = 800 * (10**-9)
zR = pi * (10**(-3)) / 2
beam = GaussianBeam(zR, lambda0)
w0 = beam.w0
k0 = beam.k
# बड़े ग्रिड पर बना अवशोषक किनारा मास्क
pad = 500
grid = 1000 + 2 * pad
X = np.linspace(-8 * w0, 8 * w0, grid)
Y = np.linspace(-8 * w0, 8 * w0, grid)
XX, YY = np.meshgrid(X, Y)
r_mask = sqrt(XX**2 + YY**2)
def gauss_edge(r, pad_size):
  sigma = 1
  r_shift = (r - 4 * w0) / w0
  return exp(-(r_shift**2) / (2 * sigma**2))
edge_mask = np.where(r_mask > 4 * w0, gauss_edge(r_mask, pad), 1)
# फील्ड और propagation के लिए कार्यशील ग्रिड
x = np.linspace(-8 * w0, 8 * w0, 2000)
y = np.linspace(-8 * w0, 8 * w0, 2000)
Xg, Yg = np.meshgrid(x, y)
r = sqrt(Xg**2 + Yg**2)
phi = arctg(Yg, Xg)
# z = zR पर प्रारंभिक फील्ड
U0 = beam.field(zR, r, phi)
# z = 6 * zR तक पहुँचने के लिए 5 * zR का propagation
U_num = propagate_sliced(U0, 5 * zR, x, y, k0, edge_mask)
# z = 6 * zR पर विश्लेषणात्मक फील्ड
U_ref = beam.field(6 * zR, r, phi)
# साधारण पिक्सेल-दूरी माप
def pix_distance(a: np.array, b: np.array):
  v = abs((a - b).flatten())
  return np.dot(v, v) / len(v)
err = pix_distance(U_num, U_ref)
print(err)
ऊपर जितना छोटा स्टेप लिया गया है, उतनी ही लंबी दूरी पर विचलन बड़ा दिखता है। शुरुआत में फील्ड साफ़ रहता है, किनारे धुँधले किए गए हैं, फिर भी अंतिम प्रोफ़ाइल अपेक्षित विश्लेषणात्मक Gaussian से नहीं मिलती।
असल में गड़बड़ी कहाँ है
यह समस्या absorbing boundary layer की नहीं है। मसला है propagation step का बहुत छोटा होना। हर स्टेप पर फील्ड पर एक FFT और एक inverse FFT लगती है। जब स्टेपों की संख्या बिना ज़रूरत बहुत बढ़ा दी जाती है, तो बार-बार के ट्रांसफ़ॉर्म संख्यात्मक त्रुटि जमा करते हैं। यह त्रुटि पूरे propagation अनुक्रम में बढ़ती जाती है और बीम के फैल जाने पर हावी हो जाती है।
उपाय: propagation स्टेप साइज़ बढ़ाएँ
स्टेप साइज़ बढ़ाने से FFT/ifft जोड़ों की संख्या घटती है और संचित त्रुटि कम होती है। इसी सेटअप में dz = 0.001 लेने पर लक्ष्य तल पर विश्लेषणात्मक फील्ड से मेल साफ़ तौर पर बेहतर मिलता है।
def propagate_sliced(U, zdist, x1d, y1d, k0, edge_mask):
  nx, ny = U.shape
  dx = (x1d[-1] - x1d[0]) / (nx - 1)
  dy = (y1d[-1] - y1d[0]) / (ny - 1)
  fx = fftfreq(nx, d=dx)
  fy = fftfreq(ny, d=dy)
  FX, FY = np.meshgrid(fx, fy)
  kx = 2 * pi * FX
  ky = 2 * pi * FY
  kz = sqrt(k0**2 - kx**2 - ky**2 + 0j)
  Ustep = U
  while zdist != 0.0:
    U_faded = Ustep * edge_mask
    dz = 0.001  # बड़ा स्टेप साइज़ यहाँ परिणाम बेहतर करता है
    dz = min(dz, zdist)
    Ustep = step_propagate(U_faded, dz, kz)
    zdist -= dz
  return Ustep
इसी pixel-distance फ़ंक्शन से सुधार को नापा जा सकता है। dz = 0.001 पर दूरी लगभग 2.35 आती है, जबकि dz = 0.00005 पर यह करीब 64.49 तक उछल जाती है। आप अपने लक्ष्य रेंज के अनुसार dz बदलकर देख सकते हैं कि दूरी कैसे बदलती है।
यह क्यों मायने रखता है
Angular spectrum propagation में स्टेप साइज़ सिर्फ़ सुविधा का पैरामीटर नहीं है। यह तय करता है कि आप कितने स्पेक्ट्रल ट्रांसफ़ॉर्म करेंगे। स्टेप बहुत छोटा हुआ तो विधि अनावश्यक अद्यतनों पर सटीकता खर्च कर देती है। स्मूद absorbing मास्क और बीम को समेटने के लिए पर्याप्त बड़ा ग्रिड होने पर भी, बहुत महीन स्टेप केवल FFT/ifft चक्रों की संख्या बढ़ाकर त्रुटि बजट पर हावी हो सकता है।
निष्कर्ष
जब stepwise FFT propagator लंबी दूरी पर विश्लेषणात्मक Gaussian से भटकने लगे, तो सबसे पहले absorbing boundary को दोष न दें। propagation स्टेप जाँचें। यहाँ दिखाए सेटअप में dz को 0.00005 से 0.001 करने पर असंगति स्पष्ट रूप से घटती है, जैसा कि साधारण pixel-distance मीट्रिक से दिखता है। पहले बड़ा स्टेप लें, विश्लेषणात्मक प्रोफ़ाइल से मिलान करें, और फिर अपनी propagation दूरी और ग्रिड के अनुसार जितना ज़रूरी हो उतना परिष्कृत करें।
यह लेख StackOverflow पर प्रश्न (Henrique Guerra द्वारा) और Henrique Guerra के उत्तर पर आधारित है।