def _accR(R, z, dens, sigma, qintr, bhMass, soft): mgepot = np.empty_like(R) pot = np.empty_like(dens) e2 = 1. - qintr**2 s2 = sigma**2 r2 = R**2 z2 = z**2 d2 = r2 + z2 for k in range(R.size): for j in range(dens.size): if (d2[k] < s2[j] / 240.**2): e = np.sqrt( e2[j] ) # pot is Integral in {u,0,1} of -D[H[R,z,u],R]/R at (R,z)=0 pot[j] = (np.arcsin(e) / e - qintr[j]) / ( 2 * e2[j] * s2[j]) # Cfr. equation (A5) elif (d2[k] < s2[j] * 245**2): pot[j] = quadva(_accelerationR_dRRcapitalh, [0., 1.], args=(r2[k], z2[k], e2[j], s2[j]))[0] else: # R acceleration in Keplerian limit (Cappellari et al. 2002) pot[j] = np.sqrt( np.pi / 2) * sigma[j] / d2[k]**1.5 # Cfr. equation (A4) mgepot[k] = np.sum(s2 * qintr * dens * pot) G = 0.00430237 # (km/s)**2 pc/Msun [6.674e-11 SI units (CODATA-10)] return -R * (4 * np.pi * G * mgepot + G * bhMass / (d2 + soft**2)**1.5)
def _accR(R, z, dens, sigma, qintr, bhMass, soft): mgepot = np.empty_like(R) pot = np.empty_like(dens) e2 = 1. - qintr**2 s2 = sigma**2 r2 = R**2 z2 = z**2 d2 = r2 + z2 for k in range(R.size): for j in range(dens.size): if (d2[k] < s2[j]/240.**2): e = np.sqrt(e2[j]) # pot is Integral in {u,0,1} of -D[H[R,z,u],R]/R at (R,z)=0 pot[j] = (np.arcsin(e)/e - qintr[j])/(2*e2[j]*s2[j]) # Cfr. equation (A5) elif (d2[k] < s2[j]*245**2): pot[j] = quadva(_accelerationR_dRRcapitalh, [0.,1.], args=(r2[k], z2[k], e2[j], s2[j]))[0] else: # R acceleration in Keplerian limit (Cappellari et al. 2002) pot[j] = np.sqrt(np.pi/2)*sigma[j]/d2[k]**1.5 # Cfr. equation (A4) mgepot[k] = np.sum(s2*qintr*dens*pot) G = 0.00430237 # (km/s)**2 pc/Msun [6.674e-11 SI units (CODATA-10)] return -R*(4*np.pi*G*mgepot + G*bhMass/(d2 + soft**2)**1.5)
def _wvrms2(x_pc, y_pc, inc, lum3d_pc, pot3d_pc, beta, tensor): ''' Calculate the surface brightness weighted vrms2 for all the points x, y --input parameters x_pc, y_pc: 1d array for x, y position in pc inc: inclination in rad lum3d_pc, pot3d_pc: N*3 array for mge coefficients, L in L_solar/pc^2, sigma in pc beta: Anisotropy paramter vactor for each luminous Gaussian component tensor: moments to be calculated, usually zz (LOS) --output paramters: wvrms2: surface brightness weighted vrms2 (1d array with the same length as x_pc) ''' wvrms2 = np.empty_like(x_pc) for i in range(len(x_pc)): ''' lhy: Change _integrand function to enable scipy integration! wvrms2[i] = quad(_integrand, 0.0, 1.0, args=(lum3d_pc[:, 0], lum3d_pc[:, 1], lum3d_pc[:, 2], pot3d_pc[:, 0], pot3d_pc[:, 1], pot3d_pc[:, 2], x_pc[i], y_pc[i], inc, beta, tensor)) ''' wvrms2[i] = quadva(_integrand, [0., 1.], args=(lum3d_pc[:, 0], lum3d_pc[:, 1], lum3d_pc[:, 2], pot3d_pc[:, 0], pot3d_pc[:, 1], pot3d_pc[:, 2], x_pc[i], y_pc[i], inc, beta, tensor))[0] # lhy This could be translated to c return wvrms2
def _surf_vlos(x1, y1, inc_deg, surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, beta, kappa, gamma, component, nsteps): """ This routine gives the projected weighted first moment Sigma*<V_los> """ # Axisymmetric deprojection of both luminous and total mass. # See equation (12)-(14) of Cappellari (2008) # inc = np.radians(inc_deg) qintr_lum = qobs_lum**2 - np.cos(inc)**2 if np.any(qintr_lum <= 0): raise RuntimeError('Inclination too low q < 0') qintr_lum = np.sqrt(qintr_lum)/np.sin(inc) if np.any(qintr_lum < 0.05): raise RuntimeError('q < 0.05 components') dens_lum = surf_lum*qobs_lum / (sigma_lum*qintr_lum*np.sqrt(2*np.pi)) qintr_pot = qobs_pot**2 - np.cos(inc)**2 if np.any(qintr_pot <= 0): raise RuntimeError('Inclination too low q < 0') qintr_pot = np.sqrt(qintr_pot)/np.sin(inc) if np.any(qintr_pot < 0.05): raise RuntimeError('q < 0.05 components') dens_pot = surf_pot*qobs_pot / (sigma_pot*qintr_pot*np.sqrt(2*np.pi)) s2_lum = sigma_lum**2 q2_lum = qintr_lum**2 s2_pot = sigma_pot**2 q2_pot = qintr_pot**2 bani = 1./(1. - beta) # Anisotropy ratio b = (sig_R/sig_z)**2 dens_lum = dens_lum[:, None, None, None] s2_lum = s2_lum[:, None, None, None] q2_lum = q2_lum[:, None, None, None] bani = bani[:, None, None, None] kappa = kappa[:, None, None, None] gamma = gamma[:, None, None, None] dens_pot = dens_pot[None, :, None, None] s2_pot = s2_pot[None, :, None, None] q2_pot = q2_pot[None, :, None, None] # Restrict LOS integration to the range where the luminosity density # is above 1/1000 of the peak value along that LOS # ms = 3*np.max(sigma_lum) sh = np.sinh(np.linspace(0, 3, 50)) z1 = ms/sh[-1]*sh # sinh sampling of z1 in [0, ms] R2 = (z1*np.sin(inc) - y1*np.cos(inc))**2 + x1**2 # R^2 Equation (25) z2 = (z1*np.cos(inc) + y1*np.sin(inc))**2 nu = dens_lum * np.exp(-0.5/s2_lum * (R2 + z2/q2_lum)) nu = np.sum(nu, axis=(0, 1, 2)) j = np.argmax(nu) w = (nu < nu[j]/1e3) & (z1 > z1[j]) # allow for negative Gaussians if np.any(w): ms = np.min(z1[w]) sb_mu1 = quadva(_los_integrand, [-ms, ms], epsrel=1e-3, args=(dens_lum, s2_lum, q2_lum, dens_pot, s2_pot, q2_pot, x1, y1, inc, bani, kappa, gamma, component, nsteps)) return sb_mu1[0] # Ignore the integral error
def _vrms2(x, y, inc_deg, surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, beta, tensor, sigmaPsf, normPsf, pixSize, pixAng, step, nrad, nang): """ This routine gives the second V moment after convolution with a PSF. The convolution is done using interpolation of the model on a polar grid, as described in Appendix A of Cappellari (2008). """ # Axisymmetric deprojection of both luminous and total mass. # See equation (12)-(14) of Cappellari (2008) # inc = np.radians(inc_deg) qintr_lum = qobs_lum**2 - np.cos(inc)**2 if np.any(qintr_lum <= 0): raise RuntimeError('Inclination too low q < 0') qintr_lum = np.sqrt(qintr_lum) / np.sin(inc) if np.any(qintr_lum < 0.05): raise RuntimeError('q < 0.05 components') dens_lum = surf_lum * qobs_lum / (sigma_lum * qintr_lum * np.sqrt(2 * np.pi)) qintr_pot = qobs_pot**2 - np.cos(inc)**2 if np.any(qintr_pot <= 0): raise RuntimeError('Inclination too low q < 0') qintr_pot = np.sqrt(qintr_pot) / np.sin(inc) if np.any(qintr_pot < 0.05): raise RuntimeError('q < 0.05 components') dens_pot = surf_pot * qobs_pot / (sigma_pot * qintr_pot * np.sqrt(2 * np.pi)) # Define parameters of polar grid for interpolation # w = sigma_lum < np.max( np.abs(x)) # Characteristic MGE axial ratio in observed range if w.sum() < 3: qmed = np.median(qobs_lum) else: qmed = np.median(qobs_lum[w]) rell = np.sqrt(x**2 + (y / qmed)**2) # Elliptical radius of input (x, y) psfConvolution = (np.max(sigmaPsf) > 0) and (pixSize > 0) # Kernel step is 1/4 of largest value between sigma(min) and 1/2 pixel side. # Kernel half size is the sum of 3*sigma(max) and 1/2 pixel diagonal. # if (nrad * nang > x.size) and (not psfConvolution): # Just calculate values xPol = x yPol = y else: # Interpolate values on polar grid if psfConvolution: # PSF convolution if step == 0: step = max(pixSize / 2., np.min(sigmaPsf)) / 4. mx = 3 * np.max(sigmaPsf) + pixSize / np.sqrt(2) else: # No convolution step = np.min(rell.clip(1)) # Minimum radius of 1pc mx = 0 # Make linear grid in log of elliptical radius RAD and eccentric anomaly ANG # See Appendix A # rmax = np.max( rell ) + mx # Major axis of ellipse containing all data + convolution logRad = np.linspace(np.log(step), np.log(rmax), nrad) # Linear grid in np.log(rell) ang = np.linspace(0, np.pi / 2, nang) # Linear grid in eccentric anomaly radGrid, angGrid = map(np.ravel, np.meshgrid(np.exp(logRad), ang)) xPol = radGrid * np.cos(angGrid) yPol = radGrid * np.sin(angGrid) * qmed # The model Vrms computation is only performed on the polar grid # which is then used to interpolate the values at any other location # wm2Pol = np.empty_like(xPol) mgePol = np.empty_like(xPol) for j in range(xPol.size): wm2Pol[j] = quadva(_integrand, [0., 1.], args=(dens_lum, sigma_lum, qintr_lum, dens_pot, sigma_pot, qintr_pot, xPol[j], yPol[j], inc, beta, tensor))[0] mgePol[j] = np.sum(surf_lum * np.exp(-0.5 / sigma_lum**2 * (xPol[j]**2 + (yPol[j] / qobs_lum)**2))) if psfConvolution: # PSF convolution nx = np.ceil(rmax / step) ny = np.ceil(rmax * qmed / step) x1 = np.linspace(-nx, nx, 2 * nx) * step y1 = np.linspace(-ny, ny, 2 * ny) * step xCar, yCar = np.meshgrid(x1, y1) # Cartesian grid for convolution # Interpolate MGE model and Vrms over cartesian grid # r1 = 0.5 * np.log( xCar**2 + (yCar / qmed)**2) # Log elliptical radius of cartesian grid e1 = np.arctan2(np.abs(yCar / qmed), np.abs(xCar)) # Eccentric anomaly of cartesian grid wm2Car = bilinear_interpolate(logRad, ang, wm2Pol.reshape(nang, nrad), r1, e1) mgeCar = bilinear_interpolate(logRad, ang, mgePol.reshape(nang, nrad), r1, e1) nk = np.ceil(mx / step) kgrid = np.linspace(-nk, nk, 2 * nk) * step xgrid, ygrid = np.meshgrid(kgrid, kgrid) # Kernel is square if pixAng != 0: xgrid, ygrid = rotate_points(xgrid, ygrid, pixAng) # Compute kernel with equation (A6) of Cappellari (2008). # Normaliztion is irrelevant here as it cancels out. # kernel = np.zeros_like(xgrid) dx = pixSize / 2 sp = np.sqrt(2) * sigmaPsf for j in range(len(sigmaPsf)): kernel += normPsf[j] \ * (special.erf((dx-xgrid)/sp[j]) + special.erf((dx+xgrid)/sp[j])) \ * (special.erf((dx-ygrid)/sp[j]) + special.erf((dx+ygrid)/sp[j])) kernel /= np.sum(kernel) # Seeing and aperture convolution with equation (A3) # muCar = signal.fftconvolve(wm2Car, kernel, mode='same') \ / signal.fftconvolve(mgeCar, kernel, mode='same') # Interpolate convolved image at observed apertures. # Aperture integration was already included in the kernel. # mu = bilinear_interpolate(x1, y1, muCar, x, y) else: # No PSF convolution muPol = wm2Pol / mgePol if nrad * nang > x.size: # Just returns values mu = muPol else: # Interpolate values r1 = 0.5 * np.log( x**2 + (y / qmed)**2) # Log elliptical radius of input (x,y) e1 = np.arctan2(np.abs(y / qmed), np.abs(x)) # Eccentric anomaly of input (x,y) mu = bilinear_interpolate(logRad, ang, muPol.reshape(nang, nrad), r1, e1) return mu
def _surf_vlos(x1, y1, inc_deg, surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, beta, kappa, gamma, component, nsteps): """ This routine gives the projected weighted first moment Sigma*<V_los> """ # Axisymmetric deprojection of both luminous and total mass. # See equation (12)-(14) of Cappellari (2008) # inc = np.radians(inc_deg) qintr_lum = qobs_lum**2 - np.cos(inc)**2 if np.any(qintr_lum <= 0): raise RuntimeError('Inclination too low q < 0') qintr_lum = np.sqrt(qintr_lum) / np.sin(inc) if np.any(qintr_lum < 0.05): raise RuntimeError('q < 0.05 components') dens_lum = surf_lum * qobs_lum / (sigma_lum * qintr_lum * np.sqrt(2 * np.pi)) qintr_pot = qobs_pot**2 - np.cos(inc)**2 if np.any(qintr_pot <= 0): raise RuntimeError('Inclination too low q < 0') qintr_pot = np.sqrt(qintr_pot) / np.sin(inc) if np.any(qintr_pot < 0.05): raise RuntimeError('q < 0.05 components') dens_pot = surf_pot * qobs_pot / (sigma_pot * qintr_pot * np.sqrt(2 * np.pi)) s2_lum = sigma_lum**2 q2_lum = qintr_lum**2 s2_pot = sigma_pot**2 q2_pot = qintr_pot**2 bani = 1. / (1. - beta) # Anisotropy ratio b = (sig_R/sig_z)**2 dens_lum = dens_lum[:, None, None, None] s2_lum = s2_lum[:, None, None, None] q2_lum = q2_lum[:, None, None, None] bani = bani[:, None, None, None] kappa = kappa[:, None, None, None] gamma = gamma[:, None, None, None] dens_pot = dens_pot[None, :, None, None] s2_pot = s2_pot[None, :, None, None] q2_pot = q2_pot[None, :, None, None] # Restrict LOS integration to the range where the luminosity density # is above 1/1000 of the peak value along that LOS # ms = 3 * np.max(sigma_lum) sh = np.sinh(np.linspace(0, 3, 50)) z1 = ms / sh[-1] * sh # sinh sampling of z1 in [0, ms] R2 = (z1 * np.sin(inc) - y1 * np.cos(inc))**2 + x1**2 # R^2 Equation (25) z2 = (z1 * np.cos(inc) + y1 * np.sin(inc))**2 nu = dens_lum * np.exp(-0.5 / s2_lum * (R2 + z2 / q2_lum)) nu = np.sum(nu, axis=(0, 1, 2)) j = np.argmax(nu) w = (nu < nu[j] / 1e3) & (z1 > z1[j]) # allow for negative Gaussians if np.any(w): ms = np.min(z1[w]) sb_mu1 = quadva(_los_integrand, [-ms, ms], epsrel=1e-3, args=(dens_lum, s2_lum, q2_lum, dens_pot, s2_pot, q2_pot, x1, y1, inc, bani, kappa, gamma, component, nsteps)) return sb_mu1[0] # Ignore the integral error
def _second_moment(R, sig_l, sig_m, lum, mass, Mbh, beta, tensor, sigmaPsf, normPsf, step, nrad, surf_l, pixSize): """ This routine gives the second V moment after convolution with a PSF. The convolution is done using interpolation of the model on a polar grid, as described in Appendix A of Cappellari (2008). """ if (max(sigmaPsf) > 0) and (pixSize > 0): # PSF convolution # Kernel step is 1/4 of largest value between sigma(min) and 1/2 pixel side. # Kernel half size is the sum of 3*sigma(max) and 1/2 pixel diagonal. # if step == 0: step = max(pixSize/2., np.min(sigmaPsf))/4. mx = 3*np.max(sigmaPsf) + pixSize/np.sqrt(2) # Make grid linear in log of radius RR # rmax = np.max(R) + mx # Radius of circle containing all data + convolution logRad = np.linspace(np.log(step), np.log(rmax), nrad) # Linear grid in log(RR) rr = np.exp(logRad) # The model Vrms computation is only performed on the radial grid # which is then used to interpolate the values at any other location # wm2Pol = np.empty_like(rr) mgePol = np.empty_like(rr) rup = 3*np.max(sig_l) for j in range(rr.size): # Integration of equation (50) wm2Pol[j] = quadva(_integrand, [rr[j], rup], args=(sig_l, sig_m, lum, mass, Mbh, rr[j], beta, tensor))[0] mgePol[j] = np.sum(surf_l * np.exp(-0.5*(rr[j]/sig_l)**2)) nx = np.ceil(rmax/step) x1 = np.linspace(-nx, nx, 2*nx)*step xCar, yCar = np.meshgrid(x1, x1) # Cartesian grid for convolution # Interpolate MGE model and Vrms over cartesian grid # r1 = 0.5*np.log(xCar**2 + yCar**2) # Log radius of cartesian grid wm2Car = np.interp(r1, logRad, wm2Pol) mgeCar = np.interp(r1, logRad, mgePol) nk = np.ceil(mx/step) kgrid = np.linspace(-nk, nk, 2*nk)*step xgrid, ygrid = np.meshgrid(kgrid, kgrid) # Kernel is square # Compute kernel with equation (A6) of Cappellari (2008). # Normalization is irrelevant here as it cancels out. # kernel = np.zeros_like(xgrid) dx = pixSize/2 sp = np.sqrt(2)*sigmaPsf for j in range(len(sigmaPsf)): kernel += normPsf[j] \ * (special.erf((dx-xgrid)/sp[j]) + special.erf((dx+xgrid)/sp[j])) \ * (special.erf((dx-ygrid)/sp[j]) + special.erf((dx+ygrid)/sp[j])) kernel /= np.sum(kernel) # Seeing and aperture convolution with equation (A3) # muCar = np.sqrt(signal.fftconvolve(wm2Car, kernel, mode='same') / signal.fftconvolve(mgeCar, kernel, mode='same')) # Interpolate convolved image at observed apertures. # Aperture integration was already included in the kernel. # mu = bilinear_interpolate(x1, x1, muCar, R/np.sqrt(2), R/np.sqrt(2)) else: # No PSF convolution: just compute values mu = np.empty_like(R) rmax = 3*np.max(sig_l) for j in range(R.size): wm2Pol = quadva(_integrand, [R[j], rmax], args=(sig_l, sig_m, lum, mass, Mbh, R[j], beta, tensor))[0] mgePol = np.sum( surf_l * np.exp(-0.5*(R[j]/sig_l)**2) ) mu[j] = np.sqrt(wm2Pol/mgePol) return mu