def apply_interface_thin(self, interface: trains.Interface, rs_center=(0, 0), shape: str = 'circle'): """Return copy of self with interface applied as a thin phase mask. Args: interface: rs_center: shape: 'circle', 'square' or None. """ dx = self.x - rs_center[0] dy = self.y - rs_center[1] rho = (dx**2 + dy**2)**0.5 assert np.isclose(self.n, interface.n1(self.lamb)) # if shape is not None: # aperture = interface.calc_aperture(dx, dy, shape) # apertured = self.mask_binary(aperture) # cropped = apertured.crop_zeros() f, gradrf = interface.calc_mask(self.lamb, rho, True) if shape is not None: aperture = interface.calc_aperture(dx, dy, shape) f *= aperture gradrf *= aperture gradxyf = mathx.divide0(dx, rho) * gradrf, mathx.divide0(dy, rho) * gradrf return self.mask(f, gradxyf, interface.n2(self.lamb)).center_q()
def recalc_gradxyE(self, gradphi): # See p116 Dane's logbook 2. gradxE = mathx.divide0((self.gradxyE[0] * self.Er.conj()).real, self.Er.conj()) + 1j * gradphi[0] * self.Er gradyE = mathx.divide0((self.gradxyE[1] * self.Er.conj()).real, self.Er.conj()) + 1j * gradphi[1] * self.Er return gradxE, gradyE
def moment(x, f, n, nrm_fac=None, axis=None): """sum[x^n*f]/nrm_fac nrm_fac defaults to sum(f).""" if nrm_fac is None: nrm_fac = f.sum(axis, keepdims=True) return mx.squeeze_leading( mx.divide0((x**n * f).sum(axis, keepdims=True), nrm_fac))
def get_xixpt(xpi): xpt = mathx.take_broadcast(xp, [xpi], axis) # Find index of xb, the last value of x which is less than or equal to xpt xib = mathx.find_last_nonzero(x <= xpt, axis) # Find index of xa, the first value of x which is greater than xpt xia = mathx.find_first_nonzero(x > xpt, axis) # If all elements of x are greater than xpt, use zeroth and first i.e. # linearly extrapolate inds = xib == -1 xib[inds] = 0 xia[inds] = 1 # Likewise if all elements of x are smaller than or equal to xpt, use # second last and last elements inds = xia == lx xib[inds] = lx - 2 xia[inds] = lx - 1 # Get before and after values of x xb = mathx.take_broadcast(x, xib, axis) xa = mathx.take_broadcast(x, xia, axis) Dx = xa - xb if suppress_degeneracy_error: delta = mathx.divide0(xpt - xb, Dx) else: assert Dx.min() > 0 delta = (xpt - xb) / Dx xixpt = xib + delta return xixpt
def __init__(self, lamb, n, zfun, rs_support, Er, gradxyE, rs_center=(0, 0), qs_center=(0, 0), polarizationxy=(1, 0)): z_center = zfun(*rs_center) Profile.__init__(self, lamb, n, z_center, rs_support, Er, gradxyE, rs_center, qs_center, polarizationxy) self.zfun = zfun self.z = zfun(self.x, self.y) self.valid = np.isfinite(self.z) self.z[~self.valid] = self.z[self.valid].mean() assert np.allclose(Er[~self.valid], 0) sumIr = self.Ir.sum() self.mean_z = (self.z * self.Ir).sum() / sumIr app_propagator = mathx.expj( (self.mean_z - self.z) * mathx.divide0(self.Igradphi[2], self.Ir)) app_propagator[np.isnan(self.z)] = 0 Er_plane = Er * app_propagator gradxyE_plane = tuple(c * app_propagator for c in gradxyE) # Approximate plane profile. self.app = PlaneProfile(lamb, n, self.mean_z, rs_support, Er_plane, gradxyE_plane, rs_center, qs_center)
def calc_normal(self, point): rho = (point[..., 0]**2 + point[..., 1]**2)**0.5 dzdrho = self.calc_dzdrho(rho) factor = mathx.divide0(dzdrho, rho, 0) return v4hb.normalize( v4hb.stack_xyzw(-point[..., 0] * factor, -point[..., 1] * factor, 1, 0) ) # return v4hb.normalize(v4hb.stack_xyzw(-point[0]*factor, -point[1]*factor, 1, 0))
def cart_to_polar(x, y, z, r_sign=1): """Convert from cartesian to polar coordinates. See polar_to_cart for defintions. """ r = r_sign*(x**2 + y**2 + z**2)**0.5 theta = np.arccos(mathx.divide0(z, r, 1)) phi = np.arctan2(y*r_sign, x*r_sign) return r, theta, phi
def calc_bessel(x, y, radius, gradr=False): rho = (x**2 + y**2)**0.5 zero = special.jn_zeros(0, 1) # See RT2 p75 for derivation norm_fac = np.pi**0.5*special.jv(1, zero)*radius z = rho*zero/radius in_beam = rho <= radius E = pyfftw.byte_align(special.jv(0, z)*in_beam/norm_fac) if gradr: gradrhoE = special.jvp(0, z, 1)*in_beam/norm_fac*zero/radius gradxE = pyfftw.byte_align(mathx.divide0(gradrhoE*x, rho)) gradyE = pyfftw.byte_align(mathx.divide0(gradrhoE*y, rho)) return E, (gradxE, gradyE) else: return E
def interpolate(self, rs_support, num_pointss, rs_center=None): assert self.zfun is not None appi = self.app.interpolate(rs_support, num_pointss, rs_center) z = self.zfun(appi.x, appi.y) # Propagate from interpolated planar profile to resampled z. Ir = appi.Ir propagator = mathx.expj( (z - appi.z) * mathx.divide0(appi.Igradphi[2], Ir)) Er = appi.Er * propagator gradxyE = tuple(c * propagator for c in appi.gradxyE) return type(self)(self.lamb, self.n, self.zfun, appi.rs_support, Er, gradxyE, appi.rs_center, appi.qs_center)
def calc_all(self, z, rho): z = np.asarray(z) rho = np.asarray(rho) w = self.waist(z) psi = self.Gouy(z) R = self.roc(z) absE = self.w_0 / w * np.exp(-(rho / w)**2) * self.absE_w z_c = mathx.divide0(rho**2, 2 * R) # try: # z_c=rho**2/(2*R) # except ZeroDivisionError: # z_c=0 phi = self.k * z_c - psi # leave k out, retarded reference frame return {'w': w, 'psi': psi, 'absE': absE, 'phi': phi, 'R': R}
def test_pbg_simple(): waist = 100e-6 n = 1 lamb = 800e-9 k = 2*np.pi*n/lamb # Totally arbitrary origin and axes. origin = np.asarray([1, 2, 3, 1]) vector = v4hb.normalize(np.asarray([1, -2, -1, 0])) axis_x = v4hb.normalize(np.asarray([1, 1, 0, 0])) mode, (axis_x, axis_y) = pbg.Mode.make(otk.rt1._lines.Line(origin, vector), axis_x, lamb / n, waist) z_R = waist**2*k/2 z = np.asarray([-10, 1, 0, 1, 10])[:, None]*z_R*0 x = np.arange(3)[:, None, None]*waist y = np.arange(3)[:, None, None, None]*waist #z = np.asarray([[z_R]]) #x = np.asarray([[waist]]) #y = np.asarray([[waist]]) rho = (x**2 + y**2)**0.5 xhat = np.asarray([1, 0, 0, 0]) yhat = np.asarray([0, 1, 0, 0]) rhohat = mathx.divide0(xhat*x + yhat*y, rho) mode_matrix = np.stack([axis_x, axis_y, vector, origin], 0) points = v4hb.concatenate_xyzw(x, y, z, 1).dot(mode_matrix) field, phi_axis, grad_phi = mode.calc_field(k, points, calc_grad_phi=True) beam = beams.FundamentalGaussian(lamb/n, w_0=waist, flux=1) field_true = beam.E(z, rho)*mathx.expj(z*k) field_true_axis = beam.E(z, 0)*mathx.expj(z*k) grad_phi_true = (beam.dphidz(z, rho) + k)*vector + beam.dphidrho(z, rho)*rhohat.dot(mode_matrix) testing.assert_allclose(abs(field), abs(field_true)) testing.assert_allclose(field, field_true) testing.assert_allclose(abs(mathx.wrap_to_pm(phi_axis - np.angle(field_true_axis), np.pi)), 0, atol=1e-6) testing.assert_allclose(grad_phi, grad_phi_true, atol=1e-6)
def calc_gaussian_1d(k, r, waist0, z=0, r0=0, q0=0, carrier=True, gradr=False): """Calculate 1D Gaussian beam profile. The result is normalized to unity amplitude at the waist (not on the axis). Args: k: Wavenumber. r: Sampling points. waist0: Beam waist. z: Axial position relative to waist. r0: Real-space offset of waist. q0: Wavenumber-space offset of waist. carrier (bool): Whether to include kz carrier phase. gradr (bool): Whether to return partial derivative w.r.t r of the field. Returns: Er, or gradrEr. """ z_R = waist0**2*k/2 waist = waist0*(1 + (z/z_R)**2)**0.5 roc = z + mathx.divide0(z_R**2, z, np.inf) # if z == 0: # roc = np.inf # else: # roc = z+z_R**2/z r1 = r0 + q0*z/k # In 1D, Gouy phase shift is halved. See # Physical origin of the Gouy phase shift, Feng and Winful, Optics Letters Vol. 27 No. 8 2001. phi = (r - r1)**2*k/(2*roc) + q0*(r - r1) - 0.5*np.arctan(z/z_R) + q0**2*z/(2*k) if carrier: phi += k*z Er = (waist0/waist)**0.5*np.exp(-((r - r1)/waist)**2 + 1j*phi) if gradr: gradrphi = (r - r1)*k/roc + q0 result = Er*(-2*(r - r1)/waist**2 + 1j*gradrphi) else: result = Er return pyfftw.byte_align(result)
def refract(self, normal, n, scale_Er=1, polarizationxy=(1, 0)): """ Args: normal (3-tuple of 2D arrays): X, y and z components of normal vector, each sampled at (self.x, self.y). n: scale_Er: Multiplicative factor - must broadcast with self.Er. polarizationxy: Transverse components of polarization of the refracted beam. Returns: """ k = 2 * np.pi / self.lamb * n normal = [nc * np.sign(normal[2]) for nc in normal] Igradphi_tangent = matseq.project_onto_plane(self.Igradphi, normal)[0] Igradphi_normal = np.maximum( (self.Ir * k)**2 - matseq.dot(Igradphi_tangent), 0)**0.5 Igradphi = [ tc + nc * Igradphi_normal for tc, nc in zip(Igradphi_tangent, normal) ] gradphi = [mathx.divide0(c, self.Ir) for c in Igradphi[:2]] gradxE, gradyE = self.recalc_gradxyE(gradphi) # Mean transverse wavenumber is intensity-weighted average of transverse gradient of phase. qs_center = np.asarray([component.sum() for component in Igradphi[:2] ]) / self.Ir.sum() profile = self.change(n=n, gradxyE=(gradxE * scale_Er, gradyE * scale_Er), qs_center=qs_center, Er=self.Er * scale_Er, polarizationxy=polarizationxy) for _ in range(3): profile = profile.center_q() return profile
def reflect(self, normal, n, scale_Er=1, polarizationxy=(1, 0)): """ Args: normal: n: scale_Er: polarizationxy: Returns: """ Igradphi_tangent, Igradphi_normal = mathx.project_onto_plane( self.Igradphi, normal) Igradphi = [ tc - nc * Igradphi_normal for tc, nc in zip(Igradphi_tangent, normal) ] gradphi = [mathx.divide0(c, self.Ir) for c in Igradphi[:2]] gradxE, gradyE = self.recalc_gradxyE(gradphi) # Mean transverse wavenumber is intensity-weighted average of transverse gradient of phase. qs_center = np.asarray([component.sum() for component in Igradphi[:2]]) / Ir.sum() # Don't understand. What is the new coordinate system? Surely should zfun = lambda x, y: 2 * self.zfun(x, y) - self.zfun(*self.rs_center) profile = self.change(n=n, gradxyE=(gradxE * scale_Er, gradyE * scale_Er), qs_center=qs_center, Er=self.Er * scale_Er, polarizationxy=polarizationxy, zfun=zfun) for _ in range(3): profile = profile.center_q() return profile
def calc_plane_to_curved_spherical_factors(k, rs_support, num_pointss, z, ms, ri_centers=(0, 0), qs_center=(0, 0), ro_centers=(0, 0), kz_mode='local_xy'): """Calculate factors for 2D Sziklas-Siegman propagation (paraxial) from flat to curved surface(s). The magnifications must be the same for all output points, but can be different for the x and y planes. The zero-oth order phase (k_z) is (arbitrarily) included in QinvTPx. Compared to regular Sziklas-Siegman procedure, the factors are complicated by a transformation that allows nonzero real and angular space centers. See derivation page 114 Dane's logbook 2. Possible optimizations: could probably shave 50% off by more aggressive use of numba to avoid calculation of intermediates. But typically the time is spent using (tensor contract) rather than calculating the factors, so it won't bring a great speedup. Args: k (scalar): Wavenumber. rs_support (scalar or pair): Real space support. num_pointss (scalar int or pair): Number of sampling points. z (array of size num_pointss): Propagation for distance. ms (scalar or pair): Magnification(s). ri_centers (pair of scalars): Center of initial real-space aperture. qs_center (pair of scalars): Center of angular space aperture. ro_centers (pair of scalars): Center of final real-space aperture. If not given, the shift from ri_centers is inferred from the propagation distance and qs_center. kz_mode (str): 'paraxial' or 'local_xy'. Returns: QinvTPx (3D array): Product propagator, inverse DFT and quadratic output factor along x. Axes are (xo, yo, kx). gradxQinvTPx (3D array): Derivative of above w.r.t output x. Note that z is constant - the derivative is along a transverse plane rather than the surface. QinvTPy (3D array): Product propagator, inverse DFT and quadratic output factor along x. Axes are (xo, yo, kx). gradyQinvTPy (3D array): Derivative of above w.r.t output x. As with gradxQinvTPx, z is constant. Tx (2D array): DFT factor. Axes are (kx, xi). Ty (2D array): DFT factor. Axes are (ky, yi). Qix (3D array): Input x quadratic phase factor vs x. Axes are (xo, yo, xi). Qiy (3D array): Input y quadratic phase factor. Axes are (xo, yo, yi). """ assert kz_mode in ('local_xy', 'paraxial') num_pointss = sa.to_scalar_pair(num_pointss) ms = sa.to_scalar_pair(ms) if np.isclose(ms, 1).any(): logger.debug('At least one magnification (%g, %g) close to 1.', ms) qs_center = sa.to_scalar_pair(qs_center) # TODO (maybe) factor out adjust_r. if kz_mode == 'paraxial': kz_center = k else: kz_center = (k**2 - (qs_center**2).sum())**0.5 z_center = np.mean(z) if ro_centers is None: ro_centers = ri_centers + qs_center / kz_center * z_center # If local_xy mode is requested then propagation distances must be scaled. Calculate scaled propagation distances # and required kz component. if kz_mode == 'paraxial': zx = z zy = z delta_kz = k else: fx, fy, delta_kz, delta_gxkz, delta_gykz = calc_quadratic_kz_correction( k, *qs_center) zx = fx * z zy = fy * z ro_supports = rs_support * ms xi, yi = sa.calc_xy(rs_support, num_pointss, ri_centers) kxi, kyi = sa.calc_kxky(rs_support, num_pointss, qs_center) # +q_curvatures) roc_x = mathx.divide0(zx, ms[0] - 1, np.inf) roc_y = mathx.divide0(zy, ms[1] - 1, np.inf) # Quadratic phase is centered at origin. Numerically, this requires evaluation of complex exponentials of size N^3. # Significant fraction of total cost of this function, but unavoidable. Qix = calc_quadratic_phase_1d(-k, xi[:, 0] - ri_centers[0], roc_x[:, :, None]) Qiy = calc_quadratic_phase_1d(-k, yi - ri_centers[1], roc_y[:, :, None]) Tx = make_fft_matrix(num_pointss[0]) if num_pointss[1] == num_pointss[0]: Ty = Tx else: Ty = make_fft_matrix(num_pointss[1]) if 0: # This was an experiment - we calculate where a given pencil beam lands based on initial position and # momentum. It didn't improve the stability of the algorithm. In a ray picture, clip factor should be 0.5. But # I found that anything below 3 gave unacceptable artefacts. clip_factor = 3 mean_rocs = z_centers / (ms - 1) xo_kx_xi = xi[:, 0] * ms[0] + ( kxi - k * ri_centers[0] / mean_rocs[0]) * z_centers[0] / k yo_ky_yi = yi * ms[1] + ( kyi[:, None] - k * ri_centers[1] / mean_rocs[1]) * z_centers[1] / k in_xo = abs(xo_kx_xi - ro_centers[0]) < ro_supports[0] * clip_factor in_yo = abs(yo_ky_yi - ro_centers[1]) < ro_supports[1] * clip_factor Txp = Tx * in_xo Typ = Ty * in_yo xo, yo = sa.calc_xy(ro_supports, num_pointss, ro_centers) roc_xo = roc_x + zx roc_yo = roc_y + zy # If in local_xy mode, then the factors which depend on xo and yo use transformed quantities, which we subsitute here. if kz_mode == 'local_xy': xo = xo + delta_gxkz * z yo = yo + delta_gykz * z # Effective kx and ky for propagation takes into account center of curvature. kxip = kxi[:, 0] - k * ri_centers[0] / roc_x[:, :, None] kyip = kyi - k * ri_centers[1] / roc_y[:, :, None] # Calculate product of propagator and inverse transform along x axis. The x axis (arbitrarily) includes delta_kz. # Could combine all exponents before exponentiation? Won't help much because the time will be dominated by the propagator # which is N^3 - the others are N^2. phi = delta_kz * z + k * (ri_centers[0]**2 / (2 * roc_x) - ri_centers[0] * xo / (roc_x * ms[0])) QinvTPx = ( calc_propagator_quadratic_1d(k * ms[0], kxip, zx[:, :, None]) * # Propagation phase scaled by magnification. calc_quadratic_phase_1d(k, xo, roc_xo)[:, :, None] * # Normal Sziklas-Siegman final quadratic phase. mathx.expj(phi[:, :, None]) * Tx.conj()[:, None, :] / # Inverse DFT. abs(ms[0])**0.5) # Magnification correction to amplitude. # Calculate product of propagator and inverse transform along x axis. Result depends on phi = k * (ri_centers[1]**2 / (2 * roc_y) - ri_centers[1] * yo / (roc_y * ms[1])) QinvTPy = ( calc_propagator_quadratic_1d(k * ms[1], kyip, zy[:, :, None]) * # Propagation phase scaled by magnification. calc_quadratic_phase_1d(k, yo, roc_yo)[:, :, None] * # Normal Sziklas-Siegman final quadratic phase. mathx.expj(phi[:, :, None]) * Ty.conj() / # Inverse DFT. abs(ms[1])**0.5) # Magnification correction to amplitude. # If local_xy mode, need translation correction factor. Could combine this with above to reduce number of N^3 expj # calls. if kz_mode == 'local_xy': QinvTPx *= mathx.expj(delta_gxkz * kxi[:, 0] / ms[0] * z[:, :, None]) QinvTPy *= mathx.expj(delta_gykz * kyi / ms[1] * z[:, :, None]) # Evaluate derivatives of the invTP factors with respect to output x and y (but constant z - not along the curved # surface). gradxQinvTPx = 1j * (k * (xo / roc_xo)[:, :, None] - ri_centers[0] * k / (roc_x[:, :, None] * ms[0]) + kxi[:, 0] / ms[0]) * QinvTPx gradyQinvTPy = 1j * (k * (yo / roc_yo)[:, :, None] - ri_centers[1] * k / (roc_y[:, :, None] * ms[1]) + kyi / ms[1]) * QinvTPy # Won't use z gradients for now - will keep for future. # gradzPx=1j*calc_kz_paraxial_1d(k*ms[0], kxip)*Px factors = QinvTPx, gradxQinvTPx, QinvTPy, gradyQinvTPy, Tx, Ty, Qix, Qiy return factors
def roc(self, z): z = np.asarray(z) return z * (1 + mathx.divide0(self.z_R, z)**2)
def line_two_points(x1, y1, x2, y2, x): return mathx.divide0(y1*(x2 - x) + y2*(x - x1), x2 - x1, float('nan'))
def calc_beam_properties(rs_support, z, Er, Igradphi, rs_center=(0, 0)): """Calculate real space and angular centroids, and curvature. Assumes that propagation is to +z. Positive ROC means diverging i.e. center of curvature is to left. Args: k: x: y: Er: Igradphi (tuple): I times gradient of phase along the x, y and z. rs_center: Ir: Returns: """ x, y = sa.calc_xy(rs_support, Er.shape, rs_center) Ir = mathx.abs_sqd(Er) sumIr = Ir.sum() # Calculate centroid. Improve the input estimate rs_center. rs_center[0], rs_center[1], varx, vary, _ = mathx.mean_and_variance2( x, y, Ir, sumIr) # Mean transverse wavenumber is intensity-weighted average of transverse gradient of phase. qs_center = np.asarray([component.sum() for component in Igradphi[:2]]) / sumIr meanz = mathx.moment(z, Ir, 1, sumIr) # Correct spatial phase for surface curvature - approximate propagation from z to meanz. Er = Er * mathx.expj((meanz - z) * mathx.divide0(Igradphi[2], Ir)) # Do this twice, since on the first pass we are using qs_center from Igradphi which is just an estimate. for _ in range(2): # Calculate phase of quadratic component at RMS distance from center. Proportional to A in Siegman IEE J. Quantum Electronics Vol. 27 # 1991. Positive means diverging. phi_cx = 0.5 * ((x - rs_center[0]) * (Igradphi[0] - Ir * qs_center[0])).sum() / sumIr phi_cy = 0.5 * ((y - rs_center[1]) * (Igradphi[1] - Ir * qs_center[1])).sum() / sumIr # Fourier transform Er with quadratic phase removed. Ek = math.fft2(Er * mathx.expj(-(x - rs_center[0])**2 * phi_cx / varx) * mathx.expj(-(y - rs_center[1])**2 * phi_cy / vary)) kx, ky = sa.calc_kxky(rs_support, Er.shape, qs_center) # Calculate mean square sizes of Fourier transform with quadratic phase removed. #qs_center[0], qs_center[1], varkxp, varkyp, _ = mathx.mean_and_variance2(kx, ky, mathx.abs_sqd(Ek)) qs_center[0] = mathx.moment(kx, abs(Ek), 1) qs_center[1] = mathx.moment(ky, abs(Ek), 1) Ik = mathx.abs_sqd(Ek) sumIk = Ik.sum() varkxp = mathx.moment(kx - qs_center[0], Ik, 2, sumIk) varkyp = mathx.moment(ky - qs_center[1], Ik, 2, sumIk) # Calculate angular variance. varkx = bvar.infer_angular_variance_spherical(varx, phi_cx, varkxp) varky = bvar.infer_angular_variance_spherical(vary, phi_cy, varkyp) return meanz, rs_center, np.asarray((varx, vary)), qs_center, np.asarray( (varkx, varky)), np.asarray((phi_cx, phi_cy))
def __init__(self, lamb: float, n: float, z_center: float, rs_support, Er, gradxyE, rs_center=(0, 0), qs_center=(0, 0), polarizationxy=(1, 0)): self.lamb = float(lamb) self.n = float(n) self.z_center = float(z_center) rs_support = sa.to_scalar_pair(rs_support) assert (rs_support > 0).all() self.rs_support = rs_support Er = np.asarray(Er).astype(complex) self.Er = Er assert not np.isnan(Er).any() assert len(gradxyE) == 2 self.gradxyE = gradxyE self.rs_center = sa.to_scalar_pair(rs_center) self.qs_center = sa.to_scalar_pair(qs_center) self.Eq = math.fft2(Er) self.k = 2 * np.pi * self.n / self.lamb self.Ir = mathx.abs_sqd(Er) sumIr = self.Ir.sum() if np.isclose(sumIr, 0): raise NullProfileError() self.Igradphi = fsq.calc_Igradphi(self.k, Er, gradxyE, self.Ir) x, y = sa.calc_xy(rs_support, Er.shape, rs_center) self.x = x self.y = y self.r_center_indices = abs(x - rs_center[0]).argmin(), abs( y - rs_center[1]).argmin() self.delta_x, self.delta_y = self.rs_support / Er.shape self.power = sumIr * self.delta_x * self.delta_y mean_x = mathx.moment(x, self.Ir, 1, sumIr) mean_y = mathx.moment(y, self.Ir, 1, sumIr) self.centroid_rs = mean_x, mean_y var_x = mathx.moment(x - rs_center[0], self.Ir, 2, sumIr) var_y = mathx.moment(y - rs_center[1], self.Ir, 2, sumIr) self.var_rs = np.asarray((var_x, var_y)) # Calculate phase of quadratic component at RMS distance from center. Proportional to A in Siegman IEE J. Quantum Electronics Vol. 27 # 1991. Positive means diverging. phi_cx = 0.5 * ( (x - rs_center[0]) * (self.Igradphi[0] - self.Ir * qs_center[0])).sum() / sumIr phi_cy = 0.5 * ( (y - rs_center[1]) * (self.Igradphi[1] - self.Ir * qs_center[1])).sum() / sumIr self.phi_cs = np.asarray((phi_cx, phi_cy)) self.rocs = mathx.divide0(self.k * self.var_rs, 2 * self.phi_cs, np.inf) xi, yi = np.unravel_index(self.Ir.argmax(), self.Ir.shape) self.peak_indices = xi, yi self.peak_Er = self.Er[xi, yi] self.peak_rs = np.asarray((self.x[xi], self.y[yi])) self.peak_qs = np.asarray([ mathx.divide0((gradxE[xi, yi] * Er[xi, yi].conj()).imag, self.Ir[xi, yi]) for gradxE in gradxyE ]) self.kz_center = math.calc_kz(self.k, *self.qs_center) # Calculate 3D vectors. vector_center = v4hb.normalize( v4hb.stack_xyzw(*self.qs_center, self.kz_center, 0)) polarizationz = -(polarizationxy * vector_center[:2]).sum() / vector_center[2] origin_center = v4hb.stack_xyzw(*self.rs_center, self.z_center, 1) polarization = polarizationxy[0], polarizationxy[1], polarizationz, 0 y = v4hb.cross(vector_center, polarization) self.frame = np.c_[polarization, y, vector_center, origin_center].T