def make_fig(self, gal, vmin=None, vmax=None): #------------------------------------------ #Plot the distribution of the burn-in process fig = plt.figure(figsize=(5, 10)) plt.subplot(2, 1, 1) plt.title(self.gal) plt.plot(self.burn_chains[:, :, 0].T) plt.ylabel(r'Chain for $\beta^{JAM}_z$') plt.subplot(2, 1, 2) plt.plot(self.burn_chains[:, :, 1].T) plt.ylabel(r'Chain for $\Upsilon$') plt.tight_layout() # This tightens up the spacing plt.savefig('figures/Burn_in/' + self.gal + '_burnchains.png') plt.close() #------------------------------------------ #Plot the distribution of final process fig = plt.figure(figsize=(5, 10)) plt.subplot(2, 1, 1) plt.title(self.gal) plt.plot(self.final_chains[:, :, 0].T) plt.ylabel(r'Chain for $\beta^{JAM}_z$') plt.subplot(2, 1, 2) plt.plot(self.final_chains[:, :, 1].T) plt.ylabel(r'Chain for $\Upsilon$') plt.tight_layout() # This tightens up the spacing plt.savefig('figures/Chains/' + self.gal + '_finalchains.png') plt.close() #------------------------------------------ # Corner Figure of the final Flatchain figure=corner.corner(self.final_flatchains, labels=[r"$\beta^{JAM}_z$", "$\Upsilon$"], \ quantiles=[0.25, 0.50, 0.75],show_titles=True, title_fmt=".3f",title_args={"fontsize": 12} ) figure.gca().annotate(self.gal+ ' JAM', xy=(0.5, 1.0), xycoords="figure fraction", xytext=(0, -5), \ textcoords="offset points", ha="center", va="top") figure.savefig('figures/Corner/' + self.gal + '_corner.png') #------------------------------------------ # Plot Vrmsbin and Vrmsmodel if (vmin is None) or (vmax is None): vmin, vmax = ss.scoreatpercentile(self.Vrmsbin, [0.5, 99.5]) fig = plt.figure(figsize=(10, 5)) plt.subplot(1, 2, 1) plot_velfield(self.xbin, self.ybin, self.Vrmsbin, vmin=vmin, vmax=vmax, flux=self.flux, colorbar=True, \ orientation='horizontal', xlab='arcsec', ylab='arcsec', nodots=True) plt.title(r"$V^{OBS}_{rms}$") plt.subplot(1, 2, 2) plot_velfield(self.xbin, self.ybin, self.Vrmsmod, vmin=vmin, vmax=vmax, flux=self.flux, colorbar=True, \ xlab='arcsec', ylab='arcsec', nodots=True) plt.title(r"$V^{MOD}_{rms}$, $\chi^2$=" + str("{0:.2f}".format(self.chi2))) plt.tight_layout() plt.savefig('figures/Vrms/' + self.gal + '_Vrms.png') plt.close()
def plot_velfield_setup(vel, v2b_xy_file, flux_scopy_file): """ Use default plotting scheme of pPXF to visualize the velocity field with 1 mag flux contours """ assert os.path.exists(v2b_xy_file), 'File {} does not exist'.format(v2b_xy_file) assert os.path.exists(flux_scopy_file), 'File {} does not exist'.format(flux_scopy_file) assert len(vel) > 0, 'Input velocity does not make sense' xbar, ybar, xnode, ynode = np.loadtxt(v2b_xy_file, unpack=True, skiprows=1) flux = np.loadtxt(flux_scopy_file, unpack=True) assert len(xbar) == len(ybar), 'Xbar is not the same length as Ybar' assert len(xbar) == len(vel), 'Xbar is not the same length as vel' assert len(xbar) == len(flux), 'Xbar is not the same length as flux' plt.clf() plt.title('Velocity') plot_velfield(xbar, ybar, vel, flux=flux, colorbar=True, label='km/s') plt.show()
def test_loess_2d(): """ Usage example for loess_2d """ n = 200 x = np.random.uniform(-1,1,n) y = np.random.uniform(-1,1,n) z = x**2 - y**2 sigz = 0.2 zran = np.random.normal(z, sigz) zout, wout = loess_2d(x, y, zran) plt.clf() plt.subplot(131) plot_velfield(x, y, z) plt.title("Underlying Function") plt.subplot(132) plot_velfield(x, y, zran) plt.title("With Noise Added") plt.subplot(133) plot_velfield(x, y, zout) plt.title("LOESS Recovery") plt.pause(1)
dVrmsbin = np.max((pdVrmsbin, dVrmsbin), axis=0) # deprojecting the velocity fields sPA = np.sin(PA_gal * np.pi / 180.) cPA = np.cos(PA_gal * np.pi / 180.) xmod = -sPA * xbin + cPA * ybin ymod = -cPA * xbin - sPA * ybin #plotting Vbin, Sbin and Vrms,obs for check fig = plt.figure(figsize=(15, 7)) plt.subplot(1, 3, 1) plot_velfield(xmod, ymod, Vbin, flux=flux_org, colorbar=True, label='km/s', orientation='horizontal', xlab='arcsec', ylab='arcsec', nodots=True) plt.title(r"$V_{obs}$") plt.subplot(1, 3, 2) plot_velfield(xmod, ymod, Sbin, flux=flux_org, colorbar=True, label='km/s', orientation='horizontal', xlab='arcsec',
def jam_axi_vel(surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, inc, mbh, distance, xbin, ybin, normpsf=1., pixang=0., pixsize=0., plot=True, vel=None, evel=None, sigmapsf=0., goodbins=None, quiet=False, beta=None, kappa=None, gamma=None, step=0., nrad=20, nang=10, rbh=0.01, vmax=None, component='z', nsteps=51, **kwargs): """ This procedure calculates a prediction for the projected mean velocity V for an anisotropic axisymmetric galaxy model. It implements the solution of the anisotropic Jeans equations presented in equation (38) of Cappellari (2008, MNRAS, 390, 71). PSF convolution is done as described in the Appendix of that paper: http://adsabs.harvard.edu/abs/2008MNRAS.390...71C CALLING SEQUENCE: velModel, kappa, chi2, flux = jam_axi_vel( surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, inc, mbh, distance, xbin, ybin, normpsf=1, pixang=0, pixsize=0, plot=True, vel=None, evel=None, sigmapsf=0, goodbins=None, quiet=False, beta=None, kappa=None, gamma=None, step=0, nrad=20, nang=10, rbh=0.01, vmax=None, component='z', nsteps=51) See the file jam_axi_vel.py for detailed documentation. """ if beta is None: beta = np.zeros_like(surf_lum) # Anisotropy parameter beta = 1 - (sig_z/sig_R)**2 (beta=0-->circle) if kappa is None: kappa = np.ones_like(surf_lum) # Anisotropy parameter: V_obs = kappa * V_iso (kappa=1-->circle) if gamma is None: gamma = np.zeros_like(surf_lum) # Anisotropy parameter gamma = 1 - (sig_phi/sig_R)^2 (gamma=0-->circle) if not (surf_lum.size == sigma_lum.size == qobs_lum.size == beta.size == gamma.size == kappa.size): raise ValueError("The luminous MGE components and anisotropies do not match") if not (surf_pot.size == sigma_pot.size == qobs_pot.size): raise ValueError("The total mass MGE components do not match") if xbin.size != ybin.size: raise ValueError("xbin and ybin do not match") if vel is not None: if evel is None: evel = np.full_like(vel, 5.) # Constant 5 km/s errors if goodbins is None: goodbins = np.ones_like(vel, dtype=bool) elif goodbins.dtype != bool: raise ValueError("goodbins must be a boolean vector") if not (xbin.size == vel.size == evel.size == goodbins.size): raise ValueError("(vel, evel, goodbins) and (xbin, ybin) do not match") sigmapsf = np.atleast_1d(sigmapsf) normpsf = np.atleast_1d(normpsf) if sigmapsf.size != normpsf.size: raise ValueError("sigmaPSF and normPSF do not match") pc = distance*np.pi/0.648 # Constant factor to convert arcsec --> pc surf_lum_pc = surf_lum surf_pot_pc = surf_pot sigma_lum_pc = sigma_lum*pc # Convert from arcsec to pc sigma_pot_pc = sigma_pot*pc # Convert from arcsec to pc xbin_pc = xbin*pc # Convert all distances to pc ybin_pc = ybin*pc pixSize_pc = pixsize*pc sigmaPsf_pc = sigmapsf*pc step_pc = step*pc # Add a Gaussian with small sigma and the same total mass as the BH. # The Gaussian provides an excellent representation of the second moments # of a point-like mass, to 1% accuracy out to a radius 2*sigmaBH. # The error increses to 14% at 1*sigmaBH, independently of the BH mass. # if mbh > 0: sigmaBH_pc = rbh*pc # Adopt for the BH just a very small size surfBH_pc = mbh/(2*np.pi*sigmaBH_pc**2) surf_pot_pc = np.append(surfBH_pc, surf_pot_pc) # Add Gaussian to potential only! sigma_pot_pc = np.append(sigmaBH_pc, sigma_pot_pc) qobs_pot = np.append(1., qobs_pot) # Make sure vectors do not have extra dimensions qobs_lum = qobs_lum.clip(0, 0.999) qobs_pot = qobs_pot.clip(0, 0.999) t = clock() velModel = _vel(xbin_pc, ybin_pc, inc, surf_lum_pc, sigma_lum_pc, qobs_lum, surf_pot_pc, sigma_pot_pc, qobs_pot, beta, kappa, gamma, sigmaPsf_pc, normpsf, pixSize_pc, pixang, step_pc, nrad, nang, component, nsteps) if not quiet: print('jam_axis_vel elapsed time sec: %.2f' % (clock() - t)) # Analytic convolution of the MGE model with an MGE circular PSF # using Equations (4,5) of Cappellari (2002, MNRAS, 333, 400) # lum = surf_lum_pc*qobs_lum*sigma_lum**2 # Luminosity/(2np.pi) of each Gaussian flux = np.zeros_like(xbin) # Total MGE surface brightness for plotting for sigp, norp in zip(sigmapsf, normpsf): # loop over the PSF Gaussians sigmaX = np.sqrt(sigma_lum**2 + sigp**2) sigmaY = np.sqrt((sigma_lum*qobs_lum)**2 + sigp**2) surfConv = lum / (sigmaX*sigmaY) # PSF-convolved in Lsun/pc**2 for srf, sx, sy in zip(surfConv, sigmaX, sigmaY): # loop over the galaxy MGE Gaussians flux += norp*srf*np.exp(-0.5*((xbin/sx)**2 + (ybin/sy)**2)) ####### Output and optional M/L fit # If RMS keyword is not given all this section is skipped if vel is None: chi2 = None else: # Only consider the good bins for the chi**2 estimation # # Scale by having the same angular momentum # in the model and in the galaxy (equation 52) # kappa2 = np.sum(np.abs(vel[goodbins]*xbin[goodbins])) \ / np.sum(np.abs(velModel[goodbins]*xbin[goodbins])) kappa = kappa*kappa2 # Rescale input kappa by the best fit # Measure the scaling one would have from a standard chi^2 fit of the V field. # This value is only used to get proper sense of rotation for the model. # y1 = rms; dy1 = erms (y1 are the data, y2 the model) # scale = total(y1*y2/dy1^2)/total(y2^2/dy1^2) (equation 51) # kappa3 = np.sum(vel[goodbins]*velModel[goodbins]/evel[goodbins]**2) \ / np.sum((velModel[goodbins]/evel[goodbins])**2) velModel *= kappa2*np.sign(kappa3) chi2 = np.sum(((vel[goodbins]-velModel[goodbins])/evel[goodbins])**2) / goodbins.sum() if not quiet: print('inc=%.1f kappa=%.2f beta_z=%.2f BH=%.2e chi2/DOF=%.3g' % (inc, kappa[0], beta[0], mbh, chi2)) mass = 2*np.pi*surf_pot_pc*qobs_pot*sigma_pot_pc**2 print('Total mass MGE %.4g:' % np.sum(mass)) if plot: vel1 = vel.copy() # Only symmetrize good bins vel1[goodbins] = symmetrize_velfield(xbin[goodbins], ybin[goodbins], vel[goodbins], sym=1) if vmax is None: vmax = stats.scoreatpercentile(np.abs(vel1[goodbins]), 99) plt.clf() plt.subplot(121) plot_velfield(xbin, ybin, vel1, vmin=-vmax, vmax=vmax, flux=flux, **kwargs) plt.title(r"Input $V_{\rm los}$") plt.subplot(122) plot_velfield(xbin, ybin, velModel, vmin=-vmax, vmax=vmax, flux=flux, **kwargs) plt.plot(xbin[~goodbins], ybin[~goodbins], 'ok', mec='white') plt.title(r"Model $V_{\rm los}$") plt.tick_params(labelleft='off') plt.subplots_adjust(wspace=0.03) return velModel, kappa, chi2, flux
def jam_axi_rms(surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, inc, mbh, distance, xbin, ybin, ml=None, normpsf=1., pixang=0., pixsize=0., plot=True, rms=None, erms=None, sigmapsf=0., goodbins=None, quiet=False, beta=None, step=0., nrad=20, nang=10, rbh=0.01, tensor='zz', vmin=None, vmax=None, **kwargs): """ This procedure calculates a prediction for the projected second velocity moment V_RMS = sqrt(V^2 + sigma^2), or optionally any of the six components of the symmetric proper motion dispersion tensor, for an anisotropic axisymmetric galaxy model. It implements the solution of the anisotropic Jeans equations presented in equation (28) and note 5 of Cappellari (2008, MNRAS, 390, 71). PSF convolution in done as described in the Appendix of that paper. http://adsabs.harvard.edu/abs/2008MNRAS.390...71C CALLING SEQUENCE: rmsModel, ml, chi2, flux = \ jam_axi_rms(surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, inc, mbh, distance, xbin, ybin, ml=None, normpsf=1, pixang=0, pixsize=0, plot=True, rms=None, erms=None, sigmapsf=0, goodbins=None, quiet=False, beta=None, step=0, nrad=20, nang=10, rbh=0.01, tensor='zz', vmin=None, vmax=None) See the file jam_axi_rms.py for detailed documentation. """ if beta is None: beta = np.zeros_like( surf_lum) # Anisotropy parameter beta = 1 - (sig_z/sig_R)**2 if not (surf_lum.size == sigma_lum.size == qobs_lum.size == beta.size): raise ValueError("The luminous MGE components do not match") if not (surf_pot.size == sigma_pot.size == qobs_pot.size): raise ValueError("The total mass MGE components do not match") if xbin.size != ybin.size: raise ValueError("xbin and ybin do not match") if rms is not None: if erms is None: erms = np.full_like(rms, np.median(rms) * 0.05) # Constant ~5% errors if goodbins is None: goodbins = np.ones_like(rms, dtype=bool) elif goodbins.dtype != bool: raise ValueError("goodbins must be a boolean vector") if not (xbin.size == rms.size == erms.size == goodbins.size): raise ValueError( "(rms, erms, goodbins) and (xbin, ybin) do not match") sigmapsf = np.atleast_1d(sigmapsf) normpsf = np.atleast_1d(normpsf) if sigmapsf.size != normpsf.size: raise ValueError("sigmaPSF and normPSF do not match") pc = distance * np.pi / 0.648 # Constant factor to convert arcsec --> pc surf_lum_pc = surf_lum surf_pot_pc = surf_pot sigma_lum_pc = sigma_lum * pc # Convert from arcsec to pc sigma_pot_pc = sigma_pot * pc # Convert from arcsec to pc xbin_pc = xbin * pc # Convert all distances to pc ybin_pc = ybin * pc pixSize_pc = pixsize * pc sigmaPsf_pc = sigmapsf * pc step_pc = step * pc # Add a Gaussian with small sigma and the same total mass as the BH. # The Gaussian provides an excellent representation of the second moments # of a point-like mass, to 1% accuracy out to a radius 2*sigmaBH. # The error increses to 14% at 1*sigmaBH, independently of the BH mass. # if mbh > 0: sigmaBH_pc = rbh * pc # Adopt for the BH just a very small size surfBH_pc = mbh / (2 * np.pi * sigmaBH_pc**2) surf_pot_pc = np.append(surfBH_pc, surf_pot_pc) # Add Gaussian to potential only! sigma_pot_pc = np.append(sigmaBH_pc, sigma_pot_pc) qobs_pot = np.append( 1., qobs_pot) # Make sure vectors do not have extra dimensions qobs_lum = qobs_lum.clip(0, 0.999) qobs_pot = qobs_pot.clip(0, 0.999) t = clock() rmsModel = _vrms2(xbin_pc, ybin_pc, inc, surf_lum_pc, sigma_lum_pc, qobs_lum, surf_pot_pc, sigma_pot_pc, qobs_pot, beta, tensor, sigmaPsf_pc, normpsf, pixSize_pc, pixang, step_pc, nrad, nang) if not quiet: print('jam_axi_rms elapsed time sec: %.2f' % (clock() - t)) if tensor in ('xx', 'yy', 'zz'): rmsModel = np.sqrt( rmsModel.clip(0)) # Return SQRT and fix possible rounding errors if tensor in ('xy', 'xz'): rmsModel *= np.sign(xbin * ybin) # Calculation was done in positive quadrant # Analytic convolution of the MGE model with an MGE circular PSF # using Equations (4,5) of Cappellari (2002, MNRAS, 333, 400) # lum = surf_lum_pc * qobs_lum * sigma_lum**2 # Luminosity/(2np.pi) of each Gaussian flux = np.zeros_like(xbin) # Total MGE surface brightness for plotting for sigp, norp in zip(sigmapsf, normpsf): # loop over the PSF Gaussians sigmaX = np.sqrt(sigma_lum**2 + sigp**2) sigmaY = np.sqrt((sigma_lum * qobs_lum)**2 + sigp**2) surfConv = lum / (sigmaX * sigmaY) # PSF-convolved in Lsun/pc**2 for srf, sx, sy in zip(surfConv, sigmaX, sigmaY): # loop over the galaxy MGE Gaussians flux += norp * srf * np.exp(-0.5 * ((xbin / sx)**2 + (ybin / sy)**2)) if rms is None: chi2 = None if ml is None: ml = 1. else: rmsModel *= np.sqrt(ml) else: if (ml is None) or (ml <= 0): # y1, dy1 = rms, erms # (y1 are the data, y2 the model) # scale = sum(y1*y2/dy1**2)/sum(y2**2/dy1**2) # (equation 51) # ml = (np.sum(rms[goodbins] * rmsModel[goodbins] / erms[goodbins]**2) / np.sum( (rmsModel[goodbins] / erms[goodbins])**2))**2 rmsModel *= np.sqrt(ml) chi2 = np.sum(((rms[goodbins] - rmsModel[goodbins]) / erms[goodbins])** 2) / goodbins.sum() if not quiet: print('inc=%.1f beta_z=%.2f M/L=%.3g BH=%.2e chi2/DOF=%.3g' % (inc, beta[0], ml, mbh * ml, chi2)) mass = 2 * np.pi * surf_pot_pc * qobs_pot * sigma_pot_pc**2 print('Total mass MGE: %.4g' % np.sum(mass * ml)) if plot: rms1 = rms.copy() # Only symmetrize good bins rms1[goodbins] = symmetrize_velfield(xbin[goodbins], ybin[goodbins], rms[goodbins]) if (vmin is None) or (vmax is None): vmin, vmax = stats.scoreatpercentile( rms1[goodbins], [0.5, 99.5]) # Could use np.percentile in Numpy 1.10 plt.clf() plt.subplot(121) plot_velfield(xbin, ybin, rms1, vmin=vmin, vmax=vmax, flux=flux, **kwargs) plt.title(r"Input $V_{\rm rms}$") plt.subplot(122) plot_velfield(xbin, ybin, rmsModel, vmin=vmin, vmax=vmax, flux=flux, **kwargs) plt.plot(xbin[~goodbins], ybin[~goodbins], 'ok', mec='white') plt.title(r"Model $V_{\rm rms}$") plt.tick_params(labelleft='off') plt.subplots_adjust(wspace=0.03) return rmsModel, ml, chi2, flux
def jam_axi_vel(surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, inc, mbh, distance, xbin, ybin, normpsf=1., pixang=0., pixsize=0., plot=True, vel=None, evel=None, sigmapsf=0., goodbins=None, quiet=False, beta=None, kappa=None, gamma=None, step=0., nrad=20, nang=10, rbh=0.01, vmax=None, component='z', nsteps=51, **kwargs): """ This procedure calculates a prediction for the projected mean velocity V for an anisotropic axisymmetric galaxy model. It implements the solution of the anisotropic Jeans equations presented in equation (38) of Cappellari (2008, MNRAS, 390, 71). PSF convolution is done as described in the Appendix of that paper: http://adsabs.harvard.edu/abs/2008MNRAS.390...71C CALLING SEQUENCE: velModel, kappa, chi2, flux = jam_axi_vel( surf_lum, sigma_lum, qobs_lum, surf_pot, sigma_pot, qobs_pot, inc, mbh, distance, xbin, ybin, normpsf=1, pixang=0, pixsize=0, plot=True, vel=None, evel=None, sigmapsf=0, goodbins=None, quiet=False, beta=None, kappa=None, gamma=None, step=0, nrad=20, nang=10, rbh=0.01, vmax=None, component='z', nsteps=51) See the file jam_axi_vel.py for detailed documentation. """ if beta is None: beta = np.zeros_like( surf_lum ) # Anisotropy parameter beta = 1 - (sig_z/sig_R)**2 (beta=0-->circle) if kappa is None: kappa = np.ones_like( surf_lum ) # Anisotropy parameter: V_obs = kappa * V_iso (kappa=1-->circle) if gamma is None: gamma = np.zeros_like( surf_lum ) # Anisotropy parameter gamma = 1 - (sig_phi/sig_R)^2 (gamma=0-->circle) if not (surf_lum.size == sigma_lum.size == qobs_lum.size == beta.size == gamma.size == kappa.size): raise ValueError( "The luminous MGE components and anisotropies do not match") if not (surf_pot.size == sigma_pot.size == qobs_pot.size): raise ValueError("The total mass MGE components do not match") if xbin.size != ybin.size: raise ValueError("xbin and ybin do not match") if vel is not None: if evel is None: evel = np.full_like(vel, 5.) # Constant 5 km/s errors if goodbins is None: goodbins = np.ones_like(vel, dtype=bool) elif goodbins.dtype != bool: raise ValueError("goodbins must be a boolean vector") if not (xbin.size == vel.size == evel.size == goodbins.size): raise ValueError( "(vel, evel, goodbins) and (xbin, ybin) do not match") sigmapsf = np.atleast_1d(sigmapsf) normpsf = np.atleast_1d(normpsf) if sigmapsf.size != normpsf.size: raise ValueError("sigmaPSF and normPSF do not match") pc = distance * np.pi / 0.648 # Constant factor to convert arcsec --> pc surf_lum_pc = surf_lum surf_pot_pc = surf_pot sigma_lum_pc = sigma_lum * pc # Convert from arcsec to pc sigma_pot_pc = sigma_pot * pc # Convert from arcsec to pc xbin_pc = xbin * pc # Convert all distances to pc ybin_pc = ybin * pc pixSize_pc = pixsize * pc sigmaPsf_pc = sigmapsf * pc step_pc = step * pc # Add a Gaussian with small sigma and the same total mass as the BH. # The Gaussian provides an excellent representation of the second moments # of a point-like mass, to 1% accuracy out to a radius 2*sigmaBH. # The error increses to 14% at 1*sigmaBH, independently of the BH mass. # if mbh > 0: sigmaBH_pc = rbh * pc # Adopt for the BH just a very small size surfBH_pc = mbh / (2 * np.pi * sigmaBH_pc**2) surf_pot_pc = np.append(surfBH_pc, surf_pot_pc) # Add Gaussian to potential only! sigma_pot_pc = np.append(sigmaBH_pc, sigma_pot_pc) qobs_pot = np.append( 1., qobs_pot) # Make sure vectors do not have extra dimensions qobs_lum = qobs_lum.clip(0, 0.999) qobs_pot = qobs_pot.clip(0, 0.999) t = clock() velModel = _vel(xbin_pc, ybin_pc, inc, surf_lum_pc, sigma_lum_pc, qobs_lum, surf_pot_pc, sigma_pot_pc, qobs_pot, beta, kappa, gamma, sigmaPsf_pc, normpsf, pixSize_pc, pixang, step_pc, nrad, nang, component, nsteps) if not quiet: print('jam_axis_vel elapsed time sec: %.2f' % (clock() - t)) # Analytic convolution of the MGE model with an MGE circular PSF # using Equations (4,5) of Cappellari (2002, MNRAS, 333, 400) # lum = surf_lum_pc * qobs_lum * sigma_lum**2 # Luminosity/(2np.pi) of each Gaussian flux = np.zeros_like(xbin) # Total MGE surface brightness for plotting for sigp, norp in zip(sigmapsf, normpsf): # loop over the PSF Gaussians sigmaX = np.sqrt(sigma_lum**2 + sigp**2) sigmaY = np.sqrt((sigma_lum * qobs_lum)**2 + sigp**2) surfConv = lum / (sigmaX * sigmaY) # PSF-convolved in Lsun/pc**2 for srf, sx, sy in zip(surfConv, sigmaX, sigmaY): # loop over the galaxy MGE Gaussians flux += norp * srf * np.exp(-0.5 * ((xbin / sx)**2 + (ybin / sy)**2)) ####### Output and optional M/L fit # If RMS keyword is not given all this section is skipped if vel is None: chi2 = None else: # Only consider the good bins for the chi**2 estimation # # Scale by having the same angular momentum # in the model and in the galaxy (equation 52) # kappa2 = np.sum(np.abs(vel[goodbins]*xbin[goodbins])) \ / np.sum(np.abs(velModel[goodbins]*xbin[goodbins])) kappa = kappa * kappa2 # Rescale input kappa by the best fit # Measure the scaling one would have from a standard chi^2 fit of the V field. # This value is only used to get proper sense of rotation for the model. # y1 = rms; dy1 = erms (y1 are the data, y2 the model) # scale = total(y1*y2/dy1^2)/total(y2^2/dy1^2) (equation 51) # kappa3 = np.sum(vel[goodbins]*velModel[goodbins]/evel[goodbins]**2) \ / np.sum((velModel[goodbins]/evel[goodbins])**2) velModel *= kappa2 * np.sign(kappa3) chi2 = np.sum(((vel[goodbins] - velModel[goodbins]) / evel[goodbins])** 2) / goodbins.sum() if not quiet: print('inc=%.1f kappa=%.2f beta_z=%.2f BH=%.2e chi2/DOF=%.3g' % (inc, kappa[0], beta[0], mbh, chi2)) mass = 2 * np.pi * surf_pot_pc * qobs_pot * sigma_pot_pc**2 print('Total mass MGE %.4g:' % np.sum(mass)) if plot: vel1 = vel.copy() # Only symmetrize good bins vel1[goodbins] = symmetrize_velfield(xbin[goodbins], ybin[goodbins], vel[goodbins], sym=1) if vmax is None: vmax = stats.scoreatpercentile(np.abs(vel1[goodbins]), 99) plt.clf() plt.subplot(121) plot_velfield(xbin, ybin, vel1, vmin=-vmax, vmax=vmax, flux=flux, **kwargs) plt.title(r"Input $V_{\rm los}$") plt.subplot(122) plot_velfield(xbin, ybin, velModel, vmin=-vmax, vmax=vmax, flux=flux, **kwargs) plt.plot(xbin[~goodbins], ybin[~goodbins], 'ok', mec='white') plt.title(r"Model $V_{\rm los}$") plt.tick_params(labelleft='off') plt.subplots_adjust(wspace=0.03) return velModel, kappa, chi2, flux