def make_image(): table = Table.read('acceptance_curve.fits') table.pprint() center = SkyCoord(83.63, 22.01, unit='deg').galactic counts_image = make_empty_image(nxpix=1000, nypix=1000, binsz=0.01, xref=center.l.deg, yref=center.b.deg, proj='TAN') bkg_image = counts_image.copy() data_store = DataStore.from_dir('$GAMMAPY_EXTRA/datasets/hess-crab4-hd-hap-prod2') for events in data_store.load_all("events"): center = events.pointing_radec.galactic livetime = events.observation_live_time_duration solid_angle = Angle(0.01, "deg") ** 2 counts_image.data += bin_events_in_image(events, counts_image).data #interp_param = dict(bounds_error=False, fill_value=None) acc_hdu = fill_acceptance_image(bkg_image.header, center, table["offset"], table["Acceptance"]) acc = Quantity(acc_hdu.data, table["Acceptance"].unit) * solid_angle * livetime bkg_image.data += acc.decompose() print(acc.decompose().sum()) counts_image.writeto("counts_image.fits", clobber=True) bkg_image.writeto("bkg_image.fits", clobber=True)
def test_saturation_water_pressure(): args_list = [ (1.e-30, None, apu.K), (1.e-30, None, apu.hPa), ] check_astro_quantities(atm.saturation_water_pressure, args_list) temp = Quantity([100, 200, 300], apu.K) press = Quantity([900, 1000, 1100], apu.hPa) press_w = Quantity( [2.57439748e-17, 3.23857740e-03, 3.55188758e+01], apu.hPa ) assert_quantity_allclose( atm.saturation_water_pressure(temp, press), press_w ) assert_quantity_allclose( atm.saturation_water_pressure( temp.to(apu.mK), press.to(apu.Pa) ), press_w )
def evaluate(self, x): """Wrapper around the evaluate method on the Astropy model classes. Parameters ---------- x : `~gammapy.utils.energy.Energy` Evaluation point """ if self.spectral_model == 'PowerLaw': flux = models.PowerLaw1D.evaluate(x, self.parameters.norm, self.parameters.reference, self.parameters.index) elif self.spectral_model == 'LogParabola': # LogParabola evaluation does not work with arrays because # there is bug when using '**' with Quantities # see https://github.com/astropy/astropy/issues/4764 flux = Quantity([models.LogParabola1D.evaluate(xx, self.parameters.norm, self.parameters.reference, self.parameters.alpha, self.parameters.beta) for xx in x]) else: raise NotImplementedError('Not implemented for model {}.'.format(self.spectral_model)) return flux.to(self.parameters.norm.unit)
def logspace(cls, vmin, vmax, nbins, unit=None): """Create axis with equally log-spaced nodes if no unit is given, it will be taken from vmax Parameters ---------- vmin : `~astropy.units.Quantity`, float Lowest value vmax : `~astropy.units.Quantity`, float Highest value bins : int Number of bins unit : `~astropy.units.UnitBase`, str Unit """ if unit is not None: vmin = Quantity(vmin, unit) vmax = Quantity(vmax, unit) else: vmin = Quantity(vmin) vmax = Quantity(vmax) unit = vmax.unit vmin = vmin.to(unit) x_min, x_max = np.log10([vmin.value, vmax.value]) vals = np.logspace(x_min, x_max, nbins) return cls(vals * unit, interpolation_mode='log')
def solid_angle(image): """Compute the solid angle of each pixel. This will only give correct results for CAR maps! Parameters ---------- image : `~astropy.io.fits.ImageHDU` Input image Returns ------- area_image : `~astropy.units.Quantity` Solid angle image (matching the input image) in steradians. """ # Area of one pixel at the equator cdelt0 = image.header['CDELT1'] cdelt1 = image.header['CDELT2'] equator_area = Quantity(abs(cdelt0 * cdelt1), 'deg2') # Compute image with fraction of pixel area at equator glat = coordinates(image)[1] area_fraction = np.cos(np.radians(glat)) result = area_fraction * equator_area.to('sr') return result
def clean_up(table_in): """Create a new table with exactly the columns / info we want. """ table = Table() v = Quantity(table_in['v'].data, table_in['v'].unit) energy = v.to('MeV', equivalencies=u.spectral()) table['energy'] = energy #vFv = Quantity(table['vFv'].data, table['vFv'].unit) #flux = (vFv / (energy ** 2)).to('cm^-2 s^-1 MeV^-1') table['energy_flux'] = table_in['vFv'] table['energy_flux_err_lo'] = table_in['ed_vFv'] table['energy_flux_err_hi'] = table_in['eu_vFv'] # Compute symmetrical error because most chi^2 fitters # can't handle asymmetrical Gaussian erros table['energy_flux_err'] = 0.5 * (table_in['eu_vFv'] + table_in['ed_vFv']) table['component'] = table_in['component'] table['paper'] = table_in['paper'] mask = table['energy_flux_err_hi'] == 0 return table
def make_bkg_cube(self, bkg_norm=True): """ Make the background image for one observation from a bkg model. Parameters ---------- bkg_norm : bool If true, apply the scaling factor from the number of counts outside the exclusion region to the bkg image """ for i_E in range(len(self.energy_reco) - 1): energy_band = Energy( [self.energy_reco[i_E].value, self.energy_reco[i_E + 1].value], self.energy_reco.unit) table = self.bkg.acceptance_curve_in_energy_band( energy_band=energy_band) center = self.obs_center.galactic bkg_hdu = fill_acceptance_image(self.header, center, table["offset"], table["Acceptance"], self.offset_band[1]) bkg_image = Quantity(bkg_hdu.data, table[ "Acceptance"].unit) * self.bkg_cube.sky_image_ref.solid_angle() * self.livetime self.bkg_cube.data[i_E, :, :] = bkg_image.decompose().value if bkg_norm: scale = self.background_norm_factor() self.bkg_cube.data = scale * self.bkg_cube.data if self.save_bkg_scale: self.table_bkg_scale.add_row([self.obs_id, scale])
class MyQuantity(Quantity): def __init__(self,coord,unit): self.q = Quantity(coord,unit) @property def value(self): return self.q.value @property def unit(self): return self.q.unit def set_value(self,value): assert isinstance(value,(float,int)) _unit = self.q.unit self.q = Quantity(value,_unit) def change_unit(self,unit): assert isinstance(unit,(str,Unit)) _newQ = self.q.to(unit) self.q = _newQ def asunit(self,unit): _q = self.q.to(unit,equivalencies=spectral()) return _q
def test_LogEnergyAxis(): from scipy.stats import gmean energy = Quantity([1, 10, 100], 'TeV') energy_axis = LogEnergyAxis(energy) energy = Quantity(gmean([1, 10]), 'TeV') pix = energy_axis.wcs_world2pix(energy.to('MeV')) assert_allclose(pix, 0.5) world = energy_axis.wcs_pix2world(pix) assert_quantity_allclose(world, energy)
def yindex(self, index): if index is None: del self.yindex return if not isinstance(index, Quantity): index = Quantity(index, unit=self._default_yunit, copy=False) self.y0 = index[0] index.regular = is_regular(index.value) if index.regular: self.dy = index[1] - index[0] else: del self.dy self._yindex = index
def _get_range_from_textfields(self, min_text, max_text, linelist_units, plot_units): amin = amax = None if min_text.hasAcceptableInput() and max_text.hasAcceptableInput(): amin = float(min_text.text()) amax = float(max_text.text()) amin = Quantity(amin, plot_units) amax = Quantity(amax, plot_units) amin = amin.to(linelist_units, equivalencies=u.spectral()) amax = amax.to(linelist_units, equivalencies=u.spectral()) return (amin, amax)
def evaluate(self, method=None, **kwargs): """Evaluate NDData Array This function provides a uniform interface to several interpolators. The evaluation nodes are given as ``kwargs``. Currently available: `~scipy.interpolate.RegularGridInterpolator`, methods: linear, nearest Parameters ---------- method : str {'linear', 'nearest'}, optional Interpolation method kwargs : dict Keys are the axis names, Values the evaluation points Returns ------- array : `~astropy.units.Quantity` Interpolated values, axis order is the same as for the NDData array """ values = [] for axis in self.axes: # Extract values for each axis, default: nodes temp = Quantity(kwargs.pop(axis.name, axis.nodes)) # Transform to correct unit temp = temp.to(axis.unit).value # Transform to match interpolation behaviour of axis values.append(np.atleast_1d(axis._interp_values(temp))) # This is to catch e.g. typos in axis names if kwargs != {}: raise ValueError("Input given for unknown axis: {}".format(kwargs)) if method is None: out = self._eval_regular_grid_interp(values) elif method == 'linear': out = self._eval_regular_grid_interp(values, method='linear') elif method == 'nearest': out = self._eval_regular_grid_interp(values, method='nearest') else: raise ValueError('Interpolator {} not available'.format(method)) # Clip interpolated values to be non-negative np.clip(out, 0, None, out=out) # Attach units to the output out = out * self.data.unit return out
def parse_value(value, default_units, equivalence=None): if isinstance(value, string_types): v = value.split(",") if len(v) == 2: value = (float(v[0]), v[1]) else: value = float(v[0]) if hasattr(value, "to_astropy"): value = value.to_astropy() if isinstance(value, Quantity): q = Quantity(value.value, value.unit) elif iterable(value): q = Quantity(value[0], value[1]) else: q = Quantity(value, default_units) return q.to(default_units, equivalencies=equivalence).value
def __init__(self, energy, rad, exposure=None, psf_value=None): self.energy = Quantity(energy).to('GeV') self.rad = Quantity(rad).to('radian') if exposure is None: self.exposure = Quantity(np.ones(len(energy)), 'cm^2 s') else: self.exposure = Quantity(exposure).to('cm^2 s') if psf_value is None: self.psf_value = Quantity(np.zeros(len(energy), len(rad)), 'sr^-1') else: self.psf_value = Quantity(psf_value).to('sr^-1') # Cache for TablePSF at each energy ... only computed when needed self._table_psf_cache = [None] * len(self.energy)
def __init__(self, energy, offset, exposure=None, psf_value=None): self.energy = Quantity(energy).to("GeV") self.offset = Quantity(offset).to("radian") if not exposure: self.exposure = Quantity(np.ones(len(energy)), "cm^2 s") else: self.exposure = Quantity(exposure).to("cm^2 s") if not psf_value: self.psf_value = Quantity(np.zeros(len(energy), len(offset)), "sr^-1") else: self.psf_value = Quantity(psf_value).to("sr^-1") # Cache for TablePSF at each energy ... only computed when needed self._table_psf_cache = [None] * len(self.energy)
class Spectrum2D(object): """ A 2D spectrum. Parameters ---------- dispersion : `astropy.units.Quantity` or array, shape (N,) Spectral dispersion axis. data : array, shape (N, M) The spectral data. unit : `astropy.units.UnitBase` or str, optional Unit for the dispersion axis. """ def __init__(self, dispersion, data, unit=None): self.dispersion = Quantity(dispersion, unit=unit) if unit is not None: self.wavelength = self.dispersion.to(angstrom) else: self.wavelength = self.dispersion self.data = data
def __init__( self, energy_lo, energy_hi, theta, sigmas, norms, energy_thresh_lo="0.1 TeV", energy_thresh_hi="100 TeV", ): self.energy_lo = Quantity(energy_lo, "TeV") self.energy_hi = Quantity(energy_hi, "TeV") ebounds = EnergyBounds.from_lower_and_upper_bounds( self.energy_lo, self.energy_hi ) self.energy = ebounds.log_centers self.theta = Quantity(theta, "deg") sigmas[0][sigmas[0] == 0] = 1 sigmas[1][sigmas[1] == 0] = 1 sigmas[2][sigmas[2] == 0] = 1 self.sigmas = sigmas self.norms = norms self.energy_thresh_lo = Quantity(energy_thresh_lo, "TeV") self.energy_thresh_hi = Quantity(energy_thresh_hi, "TeV") self._interp_norms = self._setup_interpolators(self.norms) self._interp_sigmas = self._setup_interpolators(self.sigmas)
def __str__(self): """Summary report (`str`). """ ss = '*** Observation summary ***\n' ss += 'Target position: {}\n'.format(self.target_pos) ss += 'Number of observations: {}\n'.format(len(self.obs_table)) livetime = Quantity(sum(self.obs_table['LIVETIME']), 'second') ss += 'Livetime: {:.2f}\n'.format(livetime.to('hour')) zenith = self.obs_table['ZEN_PNT'] ss += 'Zenith angle: (mean={:.2f}, std={:.2f})\n'.format( zenith.mean(), zenith.std()) offset = self.offset ss += 'Offset: (mean={:.2f}, std={:.2f})\n'.format( offset.mean(), offset.std()) return ss
def solid_angle_image(self): """Solid angle image. TODO: currently uses CDELT1 x CDELT2, which only works for cartesian images near the equator. Returns ------- solid_angle_image : `~astropy.units.Quantity` Solid angle image (steradian) """ cdelt = self.wcs.wcs.cdelt solid_angle = np.abs(cdelt[0]) * np.abs(cdelt[1]) shape = self.data.shape[:2] solid_angle = solid_angle * np.ones(shape, dtype=float) solid_angle = Quantity(solid_angle, 'deg^2') return solid_angle.to('steradian')
def test_reproject_cube(): # TODO: a better test can probably be implemented here to avoid # repeating code filenames = FermiGalacticCenter.filenames() spectral_cube = SpectralCube.read(filenames['diffuse_model']) exposure_cube = SpectralCube.read(filenames['exposure_cube']) original_cube = Quantity(np.nan_to_num(spectral_cube.data.value), spectral_cube.data.unit) spectral_cube = spectral_cube.reproject_to(exposure_cube) reprojected_cube = Quantity(np.nan_to_num(spectral_cube.data.value), spectral_cube.data.unit) # 0.5 degrees per pixel in diffuse model # 2 degrees in reprojection reference # sum of reprojected should be 1/16 of sum of original if flux-preserving expected = 0.0625 * original_cube.sum() actual = reprojected_cube.sum() assert_quantity_allclose(actual, expected, rtol=1e-2)
def __init__(self, dispersion, data, unit=None): self.dispersion = Quantity(dispersion, unit=unit) if unit is not None: self.wavelength = self.dispersion.to(angstrom) else: self.wavelength = self.dispersion self.data = data
def find_node(self, val): """Find next node Parameters ---------- val : `~astropy.units.Quantity` Lookup value """ val = Quantity(val) if not val.unit.is_equivalent(self.unit): raise ValueError("Units {} and {} do not match".format(val.unit, self.unit)) val = val.to(self.data.unit) val = np.atleast_1d(val) x1 = np.array([val] * self.nbins).transpose() x2 = np.array([self.nodes] * len(val)) temp = np.abs(x1 - x2) idx = np.argmin(temp, axis=1) return idx
def __new__(cls, value, unit=None): if (isinstance(value, QueryableAttribute) or isinstance(value, ClauseElement)): unit = Unit(unit) if unit is not None else dimensionless_unscaled value = np.array(value) value = value.view(cls) value._unit = unit return value else: return Quantity.__new__(Quantity, value, unit=unit)
def test_refractive_index(): args_list = [ (1.e-30, None, apu.K), (1.e-30, None, apu.hPa), (1.e-30, None, apu.hPa), ] check_astro_quantities(atm.refractive_index, args_list) temp = Quantity([100, 200, 300], apu.K) press = Quantity([900, 1000, 1100], apu.hPa) press_w = Quantity([200, 500, 1000], apu.hPa) refr_index = Quantity( [1.0081872, 1.0050615, 1.00443253], cnv.dimless ) assert_quantity_allclose( atm.refractive_index(temp, press, press_w), refr_index ) assert_quantity_allclose( atm.refractive_index( temp.to(apu.mK), press.to(apu.Pa), press_w.to(apu.Pa) ), refr_index )
def __init__(self, dispersion, flux, error=None, continuum=None, mask=None, unit=None, dispersion_unit=None, meta=None): _unit = flux.unit if unit is None and hasattr(flux, 'unit') else unit if isinstance(error, (Quantity, Data)): if error.unit != _unit: raise UnitsError('The error unit must be the same as the ' 'flux unit.') error = error.value elif isinstance(error, Column): if error.unit != _unit: raise UnitsError('The error unit must be the same as the ' 'flux unit.') error = error.data # Set zero error elements to NaN: if error is not None: zero = error == 0 error[zero] = np.nan # Mask these elements: if mask is not None: self.mask = mask | np.isnan(error) else: self.mask = np.isnan(error) # If dispersion is a `Quantity`, `Data`, or `Column` instance with the # unit attribute set, that unit is preserved if `dispersion_unit` is # None, but overriden otherwise self.dispersion = Quantity(dispersion, unit=dispersion_unit) if dispersion_unit is not None: self.wavelength = self.dispersion.to(angstrom) else: # Assume wavelength units: self.wavelength = self.dispersion self.flux = Data(flux, error, unit) if continuum is not None: self.continuum = Quantity(continuum, unit=unit) else: self.continuum = None self.meta = meta
# Licensed under a 3-clause BSD style license - see LICENSE.rst from __future__ import absolute_import, division, print_function, unicode_literals from numpy.testing import assert_allclose import numpy as np from astropy.units import Quantity from ...source import SNR, SNRTrueloveMcKee t = Quantity([0, 1, 10, 100, 1000, 10000], "yr") snr = SNR() snr_mckee = SNRTrueloveMcKee() def test_SNR_luminosity_tev(): """Test SNR luminosity""" reference = [0, 0, 0, 0, 1.076e+33, 1.076e+33] assert_allclose(snr.luminosity_tev(t).value, reference, rtol=1e-3) def test_SNR_radius(): """Test SNR radius""" reference = [0, 3.085e+16, 3.085e+17, 3.085e+18, 1.174e+19, 2.950e+19] assert_allclose(snr.radius(t).value, reference, rtol=1e-3) def test_SNR_radius_inner(): """Test SNR radius""" reference = (1 - 0.0914) * np.array( [0, 3.085e+16, 3.085e+17, 3.085e+18, 1.174e+19, 2.950e+19]) assert_allclose(snr.radius_inner(t).value, reference, rtol=1e-3)
def __init__(self, atom_data, ionization_data, levels=None, lines=None, macro_atom_data=None, macro_atom_references=None, zeta_data=None, collision_data=None, collision_data_temperatures=None, synpp_refs=None, photoionization_data=None): self.prepared = False # CONVERT VALUES TO CGS UNITS # Convert atomic masses to CGS # We have to use constants.u because astropy uses # different values for the unit u and the constant. # This is changed in later versions of astropy ( # the value of constants.u is used in all cases) if u.u.cgs == const.u.cgs: atom_data.loc[:, "mass"] = Quantity(atom_data["mass"].values, "u").cgs else: atom_data.loc[:, "mass"] = atom_data["mass"].values * const.u.cgs # Convert ionization energies to CGS ionization_data = ionization_data.squeeze() ionization_data[:] = Quantity(ionization_data[:], "eV").cgs # Convert energy to CGS levels.loc[:, "energy"] = Quantity(levels["energy"].values, 'eV').cgs # Sort the dataframe before indexing to improve performance lines.sort_index(inplace=True) # Create a new columns with wavelengths in the CGS units lines.loc[:, 'wavelength_cm'] = Quantity(lines['wavelength'], 'angstrom').cgs # SET ATTRIBUTES self.atom_data = atom_data self.ionization_data = ionization_data self.levels = levels self.lines = lines # Rename these (drop "_all") when `prepare_atom_data` is removed! self.macro_atom_data_all = macro_atom_data self.macro_atom_references_all = macro_atom_references self.zeta_data = zeta_data self.collision_data = collision_data self.collision_data_temperatures = collision_data_temperatures self.synpp_refs = synpp_refs self.photoionization_data = photoionization_data self._check_related() self.symbol2atomic_number = OrderedDict( zip(self.atom_data['symbol'].values, self.atom_data.index)) self.atomic_number2symbol = OrderedDict( zip(self.atom_data.index, self.atom_data['symbol']))
class EnergyDependentTablePSF(object): """Energy-dependent radially-symmetric table PSF (``gtpsf`` format). TODO: add references and explanations. Parameters ---------- energy : `~astropy.units.Quantity` Energy (1-dim) offset : `~astropy.units.Quantity` with angle units Offset angle (1-dim) exposure : `~astropy.units.Quantity` Exposure (1-dim) psf_value : `~astropy.units.Quantity` PSF (2-dim with axes: psf[energy_index, offset_index] """ def __init__(self, energy, offset, exposure=None, psf_value=None): self.energy = Quantity(energy).to("GeV") self.offset = Quantity(offset).to("radian") if not exposure: self.exposure = Quantity(np.ones(len(energy)), "cm^2 s") else: self.exposure = Quantity(exposure).to("cm^2 s") if not psf_value: self.psf_value = Quantity(np.zeros(len(energy), len(offset)), "sr^-1") else: self.psf_value = Quantity(psf_value).to("sr^-1") # Cache for TablePSF at each energy ... only computed when needed self._table_psf_cache = [None] * len(self.energy) @classmethod def from_fits(cls, hdu_list): """Create `EnergyDependentTablePSF` from ``gtpsf`` format HDU list. Parameters ---------- hdu_list : `~astropy.io.fits.HDUList` HDU list with ``THETA`` and ``PSF`` extensions. """ offset = Angle(hdu_list["THETA"].data["Theta"], "deg") energy = Quantity(hdu_list["PSF"].data["Energy"], "MeV") exposure = Quantity(hdu_list["PSF"].data["Exposure"], "cm^2 s") psf_value = Quantity(hdu_list["PSF"].data["PSF"], "sr^-1") return cls(energy, offset, exposure, psf_value) def to_fits(self): """Convert to FITS HDU list format. Returns ------- hdu_list : `~astropy.io.fits.HDUList` PSF in HDU list format. """ # TODO: write HEADER keywords as gtpsf data = self.offset theta_hdu = fits.BinTableHDU(data=data, name="Theta") data = [self.energy, self.exposure, self.psf_value] psf_hdu = fits.BinTableHDU(data=data, name="PSF") hdu_list = fits.HDUList([theta_hdu, psf_hdu]) return hdu_list @classmethod def read(cls, filename): """Create `EnergyDependentTablePSF` from ``gtpsf``-format FITS file. Parameters ---------- filename : str File name """ hdu_list = fits.open(filename) return cls.from_fits(hdu_list) def write(self, *args, **kwargs): """Write to FITS file. Calls `~astropy.io.fits.HDUList.writeto`, forwarding all arguments. """ self.to_fits().writeto(*args, **kwargs) def evaluate(self, energy=None, offset=None, interp_kwargs=None): """Interpolate the value of the `EnergyOffsetArray` at a given offset and Energy. Parameters ---------- energy : `~astropy.units.Quantity` energy value offset : `~astropy.coordinates.Angle` offset value interp_kwargs : dict option for interpolation for `~scipy.interpolate.RegularGridInterpolator` Returns ------- values : `~astropy.units.Quantity` Interpolated value """ if not interp_kwargs: interp_kwargs = dict(bounds_error=False, fill_value=None) from scipy.interpolate import RegularGridInterpolator if energy is None: energy = self.energy if offset is None: offset = self.offset energy = Energy(energy).to("TeV") offset = Angle(offset).to("deg") energy_bin = self.energy.to("TeV") offset_bin = self.offset.to("deg") points = (energy_bin, offset_bin) interpolator = RegularGridInterpolator(points, self.psf_value, **interp_kwargs) ee, off = np.meshgrid(energy.value, offset.value, indexing="ij") shape = ee.shape pix_coords = np.column_stack([ee.flat, off.flat]) data_interp = interpolator(pix_coords) return Quantity(data_interp.reshape(shape), self.psf_value.unit) def table_psf_at_energy(self, energy, interp_kwargs=None, **kwargs): """Evaluate the `EnergyOffsetArray` at one given energy. Parameters ---------- energy : `~astropy.units.Quantity` Energy interp_kwargs : dict Option for interpolation for `~scipy.interpolate.RegularGridInterpolator` Returns ------- table : `~astropy.table.Table` Table with two columns: offset, value """ psf_value = self.evaluate(energy, None, interp_kwargs)[0, :] table_psf = TablePSF(self.offset, psf_value, **kwargs) return table_psf def table_psf_in_energy_band(self, energy_band, spectral_index=2, spectrum=None, **kwargs): """Average PSF in a given energy band. Expected counts in sub energy bands given the given exposure and spectrum are used as weights. Parameters ---------- energy_band : `~astropy.units.Quantity` Energy band spectral_index : float Power law spectral index (used if spectrum=None). spectrum : callable Spectrum (callable with energy as parameter). Returns ------- psf : `TablePSF` Table PSF """ if spectrum is None: def spectrum(energy): return (energy / energy_band[0]) ** (-spectral_index) # TODO: warn if `energy_band` is outside available data. energy_idx_min, energy_idx_max = self._energy_index(energy_band) # TODO: extract this into a utility function `npred_weighted_mean()` # Compute weights for energy bins weights = np.zeros_like(self.energy.value, dtype=np.float64) for idx in range(energy_idx_min, energy_idx_max - 1): energy_min = self.energy[idx] energy_max = self.energy[idx + 1] exposure = self.exposure[idx] flux = spectrum(energy_min) weights[idx] = (exposure * flux * (energy_max - energy_min)).value # Normalize weights to sum to 1 weights = weights / weights.sum() # Compute weighted PSF value array total_psf_value = np.zeros_like(self._get_1d_psf_values(0), dtype=np.float64) for idx in range(energy_idx_min, energy_idx_max - 1): psf_value = self._get_1d_psf_values(idx) total_psf_value += weights[idx] * psf_value # TODO: add version that returns `total_psf_value` without # making a `TablePSF`. return TablePSF(self.offset, total_psf_value, **kwargs) def containment_radius(self, energy, fraction, interp_kwargs=None): """Containment radius. Parameters ---------- energy : `~astropy.units.Quantity` Energy fraction : float Containment fraction in % Returns ------- radius : `~astropy.units.Quantity` Containment radius in deg """ # TODO: useless at the moment ... support array inputs or remove! psf = self.table_psf_at_energy(energy, interp_kwargs) return psf.containment_radius(fraction) def integral(self, energy, offset_min, offset_max): """Containment fraction. Parameters ---------- energy : `~astropy.units.Quantity` Energy offset_min, offset_max : `~astropy.coordinates.Angle` Offset Returns ------- fraction : array_like Containment fraction (in range 0 .. 1) """ # TODO: useless at the moment ... support array inputs or remove! psf = self.table_psf_at_energy(energy) return psf.integral(offset_min, offset_max) def info(self): """Print basic info.""" # Summarise data members ss = array_stats_str(self.offset.to("deg"), "offset") ss += array_stats_str(self.energy, "energy") ss += array_stats_str(self.exposure, "exposure") # ss += 'integral = {0}\n'.format(self.integral()) # Print some example containment radii fractions = [0.68, 0.95] energies = Quantity([10, 100], "GeV") for energy in energies: for fraction in fractions: radius = self.containment_radius(energy=energy, fraction=fraction) ss += "{0}% containment radius at {1}: {2}\n".format(100 * fraction, energy, radius) return ss def plot_psf_vs_theta(self, filename=None, energies=[1e4, 1e5, 1e6]): """Plot PSF vs theta. Parameters ---------- TODO """ import matplotlib.pyplot as plt plt.figure(figsize=(6, 4)) for energy in energies: energy_index = self._energy_index(energy) psf = self.psf_value[energy_index, :] label = "{0} GeV".format(1e-3 * energy) x = np.hstack([-self.theta[::-1], self.theta]) y = 1e-6 * np.hstack([psf[::-1], psf]) plt.plot(x, y, lw=2, label=label) # plt.semilogy() # plt.loglog() plt.legend() plt.xlim(-0.2, 0.5) plt.xlabel("Offset (deg)") plt.ylabel("PSF (1e-6 sr^-1)") plt.tight_layout() if filename != None: plt.savefig(filename) def plot_containment_vs_energy(self, filename=None): """Plot containment versus energy.""" raise NotImplementedError import matplotlib.pyplot as plt plt.clf() if filename != None: plt.savefig(filename) def plot_exposure_vs_energy(self, filename=None): """Plot exposure versus energy.""" import matplotlib.pyplot as plt plt.figure(figsize=(4, 3)) plt.plot(self.energy, self.exposure, color="black", lw=3) plt.semilogx() plt.xlabel("Energy (MeV)") plt.ylabel("Exposure (cm^2 s)") plt.xlim(1e4 / 1.3, 1.3 * 1e6) plt.ylim(0, 1.5e11) plt.tight_layout() if filename != None: plt.savefig(filename) def _energy_index(self, energy): """Find energy array index. """ # TODO: test with array input return np.searchsorted(self.energy, energy) def _get_1d_psf_values(self, energy_index): """Get 1-dim PSF value array. Parameters ---------- energy_index : int Energy index Returns ------- psf_values : `~astropy.units.Quantity` PSF value array """ psf_values = self.psf_value[energy_index, :].flatten().copy() return psf_values def _get_1d_table_psf(self, energy_index, **kwargs): """Get 1-dim TablePSF (cached). Parameters ---------- energy_index : int Energy index Returns ------- table_psf : `TablePSF` Table PSF """ # TODO: support array_like `energy_index` here? if self._table_psf_cache[energy_index] is None: psf_value = self._get_1d_psf_values(energy_index) table_psf = TablePSF(self.offset, psf_value, **kwargs) self._table_psf_cache[energy_index] = table_psf return self._table_psf_cache[energy_index]
class EnergyDependentTablePSF(object): """Energy-dependent radially-symmetric table PSF (``gtpsf`` format). TODO: add references and explanations. Parameters ---------- energy : `~astropy.units.Quantity` Energy (1-dim) rad : `~astropy.units.Quantity` with angle units Offset angle wrt source position (1-dim) exposure : `~astropy.units.Quantity` Exposure (1-dim) psf_value : `~astropy.units.Quantity` PSF (2-dim with axes: psf[energy_index, offset_index] """ def __init__(self, energy, rad, exposure=None, psf_value=None): self.energy = Quantity(energy).to('GeV') self.rad = Quantity(rad).to('radian') if exposure is None: self.exposure = Quantity(np.ones(len(energy)), 'cm^2 s') else: self.exposure = Quantity(exposure).to('cm^2 s') if psf_value is None: self.psf_value = Quantity(np.zeros(len(energy), len(rad)), 'sr^-1') else: self.psf_value = Quantity(psf_value).to('sr^-1') # Cache for TablePSF at each energy ... only computed when needed self._table_psf_cache = [None] * len(self.energy) def __str__(self): ss = 'EnergyDependentTablePSF\n' ss += '-----------------------\n' ss += '\nAxis info:\n' ss += ' ' + array_stats_str(self.rad.to('deg'), 'rad') ss += ' ' + array_stats_str(self.energy, 'energy') # ss += ' ' + array_stats_str(self.exposure, 'exposure') # ss += 'integral = {}\n'.format(self.integral()) ss += '\nContainment info:\n' # Print some example containment radii fractions = [0.68, 0.95] energies = Quantity([10, 100], 'GeV') for fraction in fractions: rads = self.containment_radius(energies=energies, fraction=fraction) for energy, rad in zip(energies, rads): ss += ' ' + '{}% containment radius at {:3.0f}: {:.2f}\n'.format(100 * fraction, energy, rad) return ss @classmethod def from_fits(cls, hdu_list): """Create `EnergyDependentTablePSF` from ``gtpsf`` format HDU list. Parameters ---------- hdu_list : `~astropy.io.fits.HDUList` HDU list with ``THETA`` and ``PSF`` extensions. """ rad = Angle(hdu_list['THETA'].data['Theta'], 'deg') energy = Quantity(hdu_list['PSF'].data['Energy'], 'MeV') exposure = Quantity(hdu_list['PSF'].data['Exposure'], 'cm^2 s') psf_value = Quantity(hdu_list['PSF'].data['PSF'], 'sr^-1') return cls(energy, rad, exposure, psf_value) def to_fits(self): """Convert to FITS HDU list format. Returns ------- hdu_list : `~astropy.io.fits.HDUList` PSF in HDU list format. """ # TODO: write HEADER keywords as gtpsf data = self.rad theta_hdu = fits.BinTableHDU(data=data, name='Theta') data = [self.energy, self.exposure, self.psf_value] psf_hdu = fits.BinTableHDU(data=data, name='PSF') hdu_list = fits.HDUList([theta_hdu, psf_hdu]) return hdu_list @classmethod def read(cls, filename): """Create `EnergyDependentTablePSF` from ``gtpsf``-format FITS file. Parameters ---------- filename : str File name """ hdu_list = fits.open(filename) return cls.from_fits(hdu_list) def write(self, *args, **kwargs): """Write to FITS file. Calls `~astropy.io.fits.HDUList.writeto`, forwarding all arguments. """ self.to_fits().writeto(*args, **kwargs) def evaluate(self, energy=None, rad=None, interp_kwargs=None): """Evaluate the PSF at a given energy and offset Parameters ---------- energy : `~astropy.units.Quantity` energy value rad : `~astropy.coordinates.Angle` Offset wrt source position interp_kwargs : dict option for interpolation for `~scipy.interpolate.RegularGridInterpolator` Returns ------- values : `~astropy.units.Quantity` Interpolated value """ if interp_kwargs is None: interp_kwargs = dict(bounds_error=False, fill_value=None) from scipy.interpolate import RegularGridInterpolator if energy is None: energy = self.energy if rad is None: rad = self.rad energy = Energy(energy).to('TeV') rad = Angle(rad).to('deg') energy_bin = self.energy.to('TeV') rad_bin = self.rad.to('deg') points = (energy_bin, rad_bin) interpolator = RegularGridInterpolator(points, self.psf_value, **interp_kwargs) energy_grid, rad_grid = np.meshgrid(energy.value, rad.value, indexing='ij') shape = energy_grid.shape pix_coords = np.column_stack([energy_grid.flat, rad_grid.flat]) data_interp = interpolator(pix_coords) return Quantity(data_interp.reshape(shape), self.psf_value.unit) def table_psf_at_energy(self, energy, interp_kwargs=None, **kwargs): """Evaluate the `EnergyOffsetArray` at one given energy. Parameters ---------- energy : `~astropy.units.Quantity` Energy interp_kwargs : dict Option for interpolation for `~scipy.interpolate.RegularGridInterpolator` Returns ------- table : `~astropy.table.Table` Table with two columns: offset, value """ psf_value = self.evaluate(energy, None, interp_kwargs)[0, :] table_psf = TablePSF(self.rad, psf_value, **kwargs) return table_psf def kernels(self, cube, rad_max, **kwargs): """ Make a set of 2D kernel images, representing the PSF at different energies. The kernel image is evaluated on the spatial and energy grid defined by the reference sky cube. Parameters ---------- cube : `~gammapy.cube.SkyCube` Reference sky cube. rad_max `~astropy.coordinates.Angle` PSF kernel size kwargs : dict Keyword arguments passed to `EnergyDependentTablePSF.table_psf_in_energy_band()`. Returns ------- kernels : list of `~numpy.ndarray` List of 2D convolution kernels. """ energies = cube.energies(mode='edges') kernels = [] for emin, emax in zip(energies[:-1], energies[1:]): energy_band = Quantity([emin, emax]) try: psf = self.table_psf_in_energy_band(energy_band, **kwargs) kernel = psf.kernel(cube.sky_image_ref, rad_max=rad_max) except ValueError: kernel = np.nan * np.ones((1, 1)) # Dummy, means "no kernel available" kernels.append(kernel) return kernels def table_psf_in_energy_band(self, energy_band, spectral_index=2, spectrum=None, **kwargs): """Average PSF in a given energy band. Expected counts in sub energy bands given the given exposure and spectrum are used as weights. Parameters ---------- energy_band : `~astropy.units.Quantity` Energy band spectral_index : float Power law spectral index (used if spectrum=None). spectrum : callable Spectrum (callable with energy as parameter). Returns ------- psf : `TablePSF` Table PSF """ if spectrum is None: def spectrum(energy): return (energy / energy_band[0]) ** (-spectral_index) # TODO: warn if `energy_band` is outside available data. energy_idx_min, energy_idx_max = self._energy_index(energy_band) # TODO: improve this, probably by evaluating the PSF (i.e. interpolating in energy) onto a new energy grid # This is a bit of a hack, but makes sure that a PSF is given, by forcing at least one slice: if energy_idx_max - energy_idx_min < 2: # log.warning('Dubious case of PSF energy binning') # Note that below always range stop of `energy_idx_max - 1` is used! # That's why we put +2 here to make sure we have at least one bin. energy_idx_max = max(energy_idx_min + 2, energy_idx_max) # Make sure we don't step out of the energy array (doesn't help much) energy_idx_max = min(energy_idx_max, len(self.energy)) # TODO: extract this into a utility function `npred_weighted_mean()` # Compute weights for energy bins weights = np.zeros_like(self.energy.value, dtype=np.float64) for idx in range(energy_idx_min, energy_idx_max - 1): energy_min = self.energy[idx] energy_max = self.energy[idx + 1] exposure = self.exposure[idx] flux = spectrum(energy_min) weights[idx] = (exposure * flux * (energy_max - energy_min)).value # Normalize weights to sum to 1 weights = weights / weights.sum() # Compute weighted PSF value array total_psf_value = np.zeros_like(self._get_1d_psf_values(0), dtype=np.float64) for idx in range(energy_idx_min, energy_idx_max - 1): psf_value = self._get_1d_psf_values(idx) total_psf_value += weights[idx] * psf_value # TODO: add version that returns `total_psf_value` without # making a `TablePSF`. return TablePSF(self.rad, total_psf_value, **kwargs) def containment_radius(self, energies, fraction, interp_kwargs=None): """Containment radius. Parameters ---------- energies : `~astropy.units.Quantity` Energy fraction : float Containment fraction in % Returns ------- rad : `~astropy.units.Quantity` Containment radius in deg """ # TODO: figure out if there's a more efficient implementation to support # arrays of energy energies = np.atleast_1d(energies) psfs = [self.table_psf_at_energy(energy, interp_kwargs) for energy in energies] rad = [psf.containment_radius(fraction) for psf in psfs] return Quantity(rad) def integral(self, energy, rad_min, rad_max): """Containment fraction. Parameters ---------- energy : `~astropy.units.Quantity` Energy rad_min, rad_max : `~astropy.coordinates.Angle` Offset Returns ------- fraction : array_like Containment fraction (in range 0 .. 1) """ # TODO: useless at the moment ... support array inputs or remove! psf = self.table_psf_at_energy(energy) return psf.integral(rad_min, rad_max) def info(self): """Print basic info""" print(self.__str__) def plot_psf_vs_rad(self, energies=[1e4, 1e5, 1e6]): """Plot PSF vs radius. Parameters ---------- TODO """ import matplotlib.pyplot as plt plt.figure(figsize=(6, 4)) for energy in energies: energy_index = self._energy_index(energy) psf = self.psf_value[energy_index, :] label = '{} GeV'.format(1e-3 * energy) x = np.hstack([-self.rad[::-1], self.rad]) y = 1e-6 * np.hstack([psf[::-1], psf]) plt.plot(x, y, lw=2, label=label) # plt.semilogy() # plt.loglog() plt.legend() plt.xlim(-0.2, 0.5) plt.xlabel('Offset (deg)') plt.ylabel('PSF (1e-6 sr^-1)') plt.tight_layout() def plot_containment_vs_energy(self, ax=None, fractions=[0.63, 0.8, 0.95], **kwargs): """Plot containment versus energy.""" import matplotlib.pyplot as plt ax = plt.gca() if ax is None else ax energy = Energy.equal_log_spacing( self.energy.min(), self.energy.max(), 10) for fraction in fractions: rad = self.containment_radius(energy, fraction) label = '{:.1f}% Containment'.format(100 * fraction) ax.plot(energy.value, rad.value, label=label, **kwargs) ax.semilogx() ax.legend(loc='best') ax.set_xlabel('Energy (GeV)') ax.set_ylabel('Containment radius (deg)') def plot_exposure_vs_energy(self): """Plot exposure versus energy.""" import matplotlib.pyplot as plt plt.figure(figsize=(4, 3)) plt.plot(self.energy, self.exposure, color='black', lw=3) plt.semilogx() plt.xlabel('Energy (MeV)') plt.ylabel('Exposure (cm^2 s)') plt.xlim(1e4 / 1.3, 1.3 * 1e6) plt.ylim(0, 1.5e11) plt.tight_layout() def _energy_index(self, energy): """Find energy array index. """ # TODO: test with array input return np.searchsorted(self.energy, energy) def _get_1d_psf_values(self, energy_index): """Get 1-dim PSF value array. Parameters ---------- energy_index : int Energy index Returns ------- psf_values : `~astropy.units.Quantity` PSF value array """ psf_values = self.psf_value[energy_index, :].flatten().copy() where_are_NaNs = np.isnan(psf_values) # When the PSF Table is not filled (with nan), the psf estimation at a given energy crashes psf_values[where_are_NaNs] = 0 return psf_values def _get_1d_table_psf(self, energy_index, **kwargs): """Get 1-dim TablePSF (cached). Parameters ---------- energy_index : int Energy index Returns ------- table_psf : `TablePSF` Table PSF """ # TODO: support array_like `energy_index` here? if self._table_psf_cache[energy_index] is None: psf_value = self._get_1d_psf_values(energy_index) table_psf = TablePSF(self.rad, psf_value, **kwargs) self._table_psf_cache[energy_index] = table_psf return self._table_psf_cache[energy_index]
def power_law(sources: Union[BaseSource, BaseSample], outer_radius: Union[str, Quantity], inner_radius: Union[str, Quantity] = Quantity(0, 'arcsec'), redshifted: bool = False, lum_en: Quantity = Quantity([[0.5, 2.0], [0.01, 100.0]], "keV"), start_pho_index: float = 1., lo_en: Quantity = Quantity(0.3, "keV"), hi_en: Quantity = Quantity(7.9, "keV"), freeze_nh: bool = True, par_fit_stat: float = 1., lum_conf: float = 68., abund_table: str = "angr", fit_method: str = "leven", group_spec: bool = True, min_counts: int = 5, min_sn: float = None, over_sample: float = None, one_rmf: bool = True, num_cores: int = NUM_CORES, timeout: Quantity = Quantity(1, 'hr')): """ This is a convenience function for fitting a tbabs absorbed powerlaw (or zpowerlw if redshifted is selected) to source spectra, with a multiplicative constant included to deal with different spectrum normalisations (constant*tbabs*powerlaw, or constant*tbabs*zpowerlw). :param List[BaseSource] sources: A single source object, or a sample of sources. :param str/Quantity outer_radius: The name or value of the outer radius of the region that the desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster, or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any inner radius will be ignored. If you are fitting for multiple sources then you can also pass a Quantity with one entry per source. :param str/Quantity inner_radius: The name or value of the outer radius of the region that the desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster, or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any inner radius will be ignored. By default this is zero arcseconds, resulting in a circular spectrum. If you are fitting for multiple sources then you can also pass a Quantity with one entry per source. :param bool redshifted: Whether the powerlaw that includes redshift (zpowerlw) should be used. :param Quantity lum_en: Energy bands in which to measure luminosity. :param float start_pho_index: The starting value for the photon index of the powerlaw. :param Quantity lo_en: The lower energy limit for the data to be fitted. :param Quantity hi_en: The upper energy limit for the data to be fitted. :param bool freeze_nh: Whether the hydrogen column density should be frozen. :param start_pho_index: :param float par_fit_stat: The delta fit statistic for the XSPEC 'error' command, default is 1.0 which should be equivelant to 1sigma errors if I've understood (https://heasarc.gsfc.nasa.gov/xanadu/xspec /manual/XSerror.html) correctly. :param float lum_conf: The confidence level for XSPEC luminosity measurements. :param str abund_table: The abundance table to use for the fit. :param str fit_method: The XSPEC fit method to use. :param bool group_spec: A boolean flag that sets whether generated spectra are grouped or not. :param float min_counts: If generating a grouped spectrum, this is the minimum number of counts per channel. To disable minimum counts set this parameter to None. :param float min_sn: If generating a grouped spectrum, this is the minimum signal to noise in each channel. To disable minimum signal to noise set this parameter to None. :param float over_sample: The minimum energy resolution for each group, set to None to disable. e.g. if over_sample=3 then the minimum width of a group is 1/3 of the resolution FWHM at that energy. :param bool one_rmf: This flag tells the method whether it should only generate one RMF for a particular ObsID-instrument combination - this is much faster in some circumstances, however the RMF does depend slightly on position on the detector. :param int num_cores: The number of cores to use (if running locally), default is set to 90% of available. :param Quantity timeout: The amount of time each individual fit is allowed to run for, the default is one hour. Please note that this is not a timeout for the entire fitting process, but a timeout to individual source fits. """ sources, inn_rad_vals, out_rad_vals = _pregen_spectra( sources, outer_radius, inner_radius, group_spec, min_counts, min_sn, over_sample, one_rmf, num_cores) sources = _check_inputs(sources, lum_en, lo_en, hi_en, fit_method, abund_table, timeout) # This function is for a set model, either absorbed powerlaw or absorbed zpowerlw # These will be inserted into the general XSPEC script template, so lists of parameters need to be in the form # of TCL lists. lum_low_lims = "{" + " ".join(lum_en[:, 0].to("keV").value.astype(str)) + "}" lum_upp_lims = "{" + " ".join(lum_en[:, 1].to("keV").value.astype(str)) + "}" if redshifted: model = "constant*tbabs*zpowerlw" par_names = "{factor nH PhoIndex Redshift norm}" else: model = "constant*tbabs*powerlaw" par_names = "{factor nH PhoIndex norm}" script_paths = [] outfile_paths = [] src_inds = [] for src_ind, source in enumerate(sources): spec_objs = source.get_spectra(out_rad_vals[src_ind], inner_radius=inn_rad_vals[src_ind], group_spec=group_spec, min_counts=min_counts, min_sn=min_sn, over_sample=over_sample) # This is because many other parts of this function assume that spec_objs is iterable, and in the case of # a source with only a single valid instrument for a single valid observation this may not be the case if isinstance(spec_objs, Spectrum): spec_objs = [spec_objs] if len(spec_objs) == 0: raise NoProductAvailableError( "There are no matching spectra for {s}, you " "need to generate them first!".format(s=source.name)) # Turn spectra paths into TCL style list for substitution into template specs = "{" + " ".join([spec.path for spec in spec_objs]) + "}" # For this model, we have to know the redshift of the source. if redshifted and source.redshift is None: raise ValueError( "You cannot supply a source without a redshift if you have elected to fit zpowerlw." ) elif redshifted and source.redshift is not None: par_values = "{{{0} {1} {2} {3} {4}}}".format( 1., source.nH.to("10^22 cm^-2").value, start_pho_index, source.redshift, 1.) else: par_values = "{{{0} {1} {2} {3}}}".format( 1., source.nH.to("10^22 cm^-2").value, start_pho_index, 1.) # Set up the TCL list that defines which parameters are frozen, dependant on user input if redshifted and freeze_nh: freezing = "{F T F T F}" elif not redshifted and freeze_nh: freezing = "{F T F F}" elif redshifted and not freeze_nh: freezing = "{F F F T F}" elif not redshifted and not freeze_nh: freezing = "{F F F F}" # Set up the TCL list that defines which parameters are linked across different spectra, # dependant on user input if redshifted: linking = "{F T T T T}" else: linking = "{F T T T}" # If the powerlaw with redshift has been chosen, then we use the redshift attached to the source object # If not we just pass a filler redshift and the luminosities are invalid if redshifted or (not redshifted and source.redshift is not None): z = source.redshift else: z = 1 warnings.warn( "{s} has no redshift information associated, so luminosities from this fit" " will be invalid, as redshift has been set to one.".format( s=source.name)) out_file, script_file = _write_xspec_script( source, spec_objs[0].storage_key, model, abund_table, fit_method, specs, lo_en, hi_en, par_names, par_values, linking, freezing, par_fit_stat, lum_low_lims, lum_upp_lims, lum_conf, z, False, "{}", "{}", "{}", "{}", True) # If the fit has already been performed we do not wish to perform it again try: res = source.get_results(out_rad_vals[src_ind], model, inn_rad_vals[src_ind], None, group_spec, min_counts, min_sn, over_sample) except ModelNotAssociatedError: script_paths.append(script_file) outfile_paths.append(out_file) src_inds.append(src_ind) run_type = "fit" return script_paths, outfile_paths, num_cores, run_type, src_inds, None, timeout
def rsun_obs(self): """ Returns the solar radius as measured by EIT in arcseconds. """ return Quantity(self.meta['solar_r'] * self.meta['cdelt1'], 'arcsec')
def time_delta(self): """GTI durations in seconds (`~astropy.units.Quantity`).""" start = self.table["START"].astype("float64") stop = self.table["STOP"].astype("float64") return Quantity(stop - start, "second")
def validate(cls, v): v = Quantity(v) if v.unit.physical_type != "energy": raise ValueError(f"Invalid unit for energy: {v.unit!r}") return v
def __init__(self, lo, hi, **kwargs): self.lo = Quantity(lo) self.hi = Quantity(hi) super(BinnedDataAxis, self).__init__(None, **kwargs)
def test_model_plot(self): model = self.source.spectral_model erange = Quantity([1, 10], 'TeV') model.plot(erange)
def pointing_zen(self): """Pointing zenith angle sky (`~astropy.units.Quantity`).""" return Quantity(self.obs_info["ZEN_PNT"], unit="deg")
def tstop(self): """Observation stop time (`~astropy.time.Time`).""" met_ref = time_ref_from_dict(self.data_store.obs_table.meta) met = Quantity(self.obs_info["TSTOP"].astype("float64"), "second") time = met_ref + met return time
def uncertainty(self): return Quantity(self.layer.uncertainty.array, unit=self.layer.units[1]).to( self._plot_units[1], equivalencies=spectral_density(self.dispersion))
def make_test_observation_table(observatory_name='HESS', n_obs=10, az_range=Angle([0, 360], 'deg'), alt_range=Angle([45, 90], 'deg'), date_range=(Time('2010-01-01'), Time('2015-01-01')), use_abs_time=False, n_tels_range=(3, 4), random_state='random-seed'): """Make a test observation table. Create an observation table following a specific pattern. For the moment, only random observation tables are created. The observation table is created according to a specific observatory, and randomizing the observation pointingpositions in a specified az-alt range. If a *date_range* is specified, the starting time of the observations will be restricted to the specified interval. These parameters are interpreted as date, the precise hour of the day is ignored, unless the end date is closer than 1 day to the starting date, in which case, the precise time of the day is also considered. In addition, a range can be specified for the number of telescopes. Parameters ---------- observatory_name : str, optional Name of the observatory; a list of choices is given in `~gammapy.data.observatory_locations`. n_obs : int, optional Number of observations for the obs table. az_range : `~astropy.coordinates.Angle`, optional Azimuth angle range (start, end) for random generation of observation pointing positions. alt_range : `~astropy.coordinates.Angle`, optional Altitude angle range (start, end) for random generation of observation pointing positions. date_range : `~astropy.time.Time`, optional Date range (start, end) for random generation of observation start time. use_abs_time : bool, optional Use absolute UTC times instead of [MET]_ seconds after the reference. n_tels_range : int, optional Range (start, end) of number of telescopes participating in the observations. random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}, optional Defines random number generator initialisation. Passed to `~gammapy.utils.random.get_random_state`. Returns ------- obs_table : `~gammapy.data.ObservationTable` Observation table. """ from ..data import ObservationTable, observatory_locations random_state = get_random_state(random_state) n_obs_start = 1 obs_table = ObservationTable() # build a time reference as the start of 2010 dateref = Time('2010-01-01T00:00:00') dateref_mjd_fra, dateref_mjd_int = np.modf(dateref.mjd) # define table header obs_table.meta['OBSERVATORY_NAME'] = observatory_name obs_table.meta['MJDREFI'] = dateref_mjd_int obs_table.meta['MJDREFF'] = dateref_mjd_fra if use_abs_time: # show the observation times in UTC obs_table.meta['TIME_FORMAT'] = 'absolute' else: # show the observation times in seconds after the reference obs_table.meta['TIME_FORMAT'] = 'relative' header = obs_table.meta # obs id obs_id = np.arange(n_obs_start, n_obs_start + n_obs) obs_table['OBS_ID'] = obs_id # obs time: 30 min ontime = Quantity(30. * np.ones_like(obs_id), 'minute').to('second') obs_table['ONTIME'] = ontime # livetime: 25 min time_live = Quantity(25. * np.ones_like(obs_id), 'minute').to('second') obs_table['LIVETIME'] = time_live # start time # - random points between the start of 2010 and the end of 2014 (unless # otherwise specified) # - using the start of 2010 as a reference time for the header of the table # - observations restrict to night time (only if specified time interval is # more than 1 day) # - considering start of astronomical day at midday: implicit in setting # the start of the night, when generating random night hours datestart = date_range[0] dateend = date_range[1] time_start = random_state.uniform(datestart.mjd, dateend.mjd, len(obs_id)) time_start = Time(time_start, format='mjd', scale='utc') # check if time interval selected is more than 1 day if (dateend - datestart).jd > 1.: # keep only the integer part (i.e. the day, not the fraction of the day) time_start_f, time_start_i = np.modf(time_start.mjd) time_start = Time(time_start_i, format='mjd', scale='utc') # random generation of night hours: 6 h (from 22 h to 4 h), leaving 1/2 h # time for the last run to finish night_start = Quantity(22., 'hour') night_duration = Quantity(5.5, 'hour') hour_start = random_state.uniform( night_start.value, night_start.value + night_duration.value, len(obs_id)) hour_start = Quantity(hour_start, 'hour') # add night hour to integer part of MJD time_start += hour_start if use_abs_time: # show the observation times in UTC time_start = Time(time_start.isot) else: # show the observation times in seconds after the reference time_start = time_relative_to_ref(time_start, header) # converting to quantity (better treatment of units) time_start = Quantity(time_start.sec, 'second') obs_table['TSTART'] = time_start # stop time # calculated as TSTART + ONTIME if use_abs_time: time_stop = Time(obs_table['TSTART']) time_stop += TimeDelta(obs_table['ONTIME']) else: time_stop = TimeDelta(obs_table['TSTART']) time_stop += TimeDelta(obs_table['ONTIME']) # converting to quantity (better treatment of units) time_stop = Quantity(time_stop.sec, 'second') obs_table['TSTOP'] = time_stop # az, alt # random points in a portion of sphere; default: above 45 deg altitude az, alt = sample_sphere(size=len(obs_id), lon_range=az_range, lat_range=alt_range, random_state=random_state) az = Angle(az, 'deg') alt = Angle(alt, 'deg') obs_table['AZ'] = az obs_table['ALT'] = alt # RA, dec # derive from az, alt taking into account that alt, az represent the values # at the middle of the observation, i.e. at time_ref + (TIME_START + TIME_STOP)/2 # (or better: time_ref + TIME_START + (TIME_OBSERVATION/2)) # in use_abs_time mode, the time_ref should not be added, since it's already included # in TIME_START and TIME_STOP az = Angle(obs_table['AZ']) alt = Angle(obs_table['ALT']) if use_abs_time: obstime = Time(obs_table['TSTART']) obstime += TimeDelta(obs_table['ONTIME']) / 2. else: obstime = time_ref_from_dict(obs_table.meta) obstime += TimeDelta(obs_table['TSTART']) obstime += TimeDelta(obs_table['ONTIME']) / 2. location = observatory_locations[observatory_name] altaz_frame = AltAz(obstime=obstime, location=location) alt_az_coord = SkyCoord(az, alt, frame=altaz_frame) sky_coord = alt_az_coord.transform_to('icrs') obs_table['RA'] = sky_coord.ra obs_table['DEC'] = sky_coord.dec # positions # number of telescopes # random integers in a specified range; default: between 3 and 4 n_tels = random_state.randint(n_tels_range[0], n_tels_range[1] + 1, len(obs_id)) obs_table['N_TELS'] = n_tels # muon efficiency # random between 0.6 and 1.0 muoneff = random_state.uniform(low=0.6, high=1.0, size=len(obs_id)) obs_table['MUONEFF'] = muoneff return obs_table
def time_start(self): """GTI start times (`~astropy.time.Time`).""" met = Quantity(self.table["START"].astype("float64"), "second") return self.time_ref + met
def test_solid_angle_image(self): actual = self.spectral_cube.solid_angle_image[10][30] expected = Quantity(self.spectral_cube.wcs.wcs.cdelt[:-1].prod(), 'deg2') assert_quantity(actual, expected.to('sr'), rtol=1e-4)
class EnergyDependentMultiGaussPSF: """ Triple Gauss analytical PSF depending on energy and theta. To evaluate the PSF call the ``to_energy_dependent_table_psf`` or ``psf_at_energy_and_theta`` methods. Parameters ---------- energy_lo : `~astropy.units.Quantity` Lower energy boundary of the energy bin. energy_hi : `~astropy.units.Quantity` Upper energy boundary of the energy bin. theta : `~astropy.units.Quantity` Center values of the theta bins. sigmas : list of 'numpy.ndarray' Triple Gauss sigma parameters, where every entry is a two dimensional 'numpy.ndarray' containing the sigma value for every given energy and theta. norms : list of 'numpy.ndarray' Triple Gauss norm parameters, where every entry is a two dimensional 'numpy.ndarray' containing the norm value for every given energy and theta. Norm corresponds to the value of the Gaussian at theta = 0. energy_thresh_lo : `~astropy.units.Quantity` Lower save energy threshold of the psf. energy_thresh_hi : `~astropy.units.Quantity` Upper save energy threshold of the psf. Examples -------- Plot R68 of the PSF vs. theta and energy: .. plot:: :include-source: import matplotlib.pyplot as plt from gammapy.irf import EnergyDependentMultiGaussPSF filename = '$GAMMAPY_DATA/tests/unbundled/irfs/psf.fits' psf = EnergyDependentMultiGaussPSF.read(filename, hdu='POINT SPREAD FUNCTION') psf.plot_containment(0.68, show_safe_energy=False) plt.show() """ def __init__( self, energy_lo, energy_hi, theta, sigmas, norms, energy_thresh_lo="0.1 TeV", energy_thresh_hi="100 TeV", ): self.energy_lo = Quantity(energy_lo, "TeV") self.energy_hi = Quantity(energy_hi, "TeV") ebounds = EnergyBounds.from_lower_and_upper_bounds( self.energy_lo, self.energy_hi ) self.energy = ebounds.log_centers self.theta = Quantity(theta, "deg") sigmas[0][sigmas[0] == 0] = 1 sigmas[1][sigmas[1] == 0] = 1 sigmas[2][sigmas[2] == 0] = 1 self.sigmas = sigmas self.norms = norms self.energy_thresh_lo = Quantity(energy_thresh_lo, "TeV") self.energy_thresh_hi = Quantity(energy_thresh_hi, "TeV") self._interp_norms = self._setup_interpolators(self.norms) self._interp_sigmas = self._setup_interpolators(self.sigmas) def _setup_interpolators(self, values_list): interps = [] for values in values_list: interp = ScaledRegularGridInterpolator( points=(self.theta, self.energy), values=values ) interps.append(interp) return interps @classmethod def read(cls, filename, hdu="PSF_2D_GAUSS"): """Create `EnergyDependentMultiGaussPSF` from FITS file. Parameters ---------- filename : str File name """ filename = make_path(filename) with fits.open(str(filename), memmap=False) as hdulist: psf = cls.from_fits(hdulist[hdu]) return psf @classmethod def from_fits(cls, hdu): """Create `EnergyDependentMultiGaussPSF` from HDU list. Parameters ---------- hdu : `~astropy.io.fits.BintableHDU` HDU """ energy_lo = Quantity(hdu.data["ENERG_LO"][0], "TeV") energy_hi = Quantity(hdu.data["ENERG_HI"][0], "TeV") theta = Angle(hdu.data["THETA_LO"][0], "deg") # Get sigmas shape = (len(theta), len(energy_hi)) sigmas = [] for key in ["SIGMA_1", "SIGMA_2", "SIGMA_3"]: sigma = hdu.data[key].reshape(shape).copy() sigmas.append(sigma) # Get amplitudes norms = [] for key in ["SCALE", "AMPL_2", "AMPL_3"]: norm = hdu.data[key].reshape(shape).copy() norms.append(norm) opts = {} try: opts["energy_thresh_lo"] = Quantity(hdu.header["LO_THRES"], "TeV") opts["energy_thresh_hi"] = Quantity(hdu.header["HI_THRES"], "TeV") except KeyError: pass return cls(energy_lo, energy_hi, theta, sigmas, norms, **opts) def to_fits(self): """ Convert psf table data to FITS hdu list. Returns ------- hdu_list : `~astropy.io.fits.HDUList` PSF in HDU list format. """ # Set up data names = [ "ENERG_LO", "ENERG_HI", "THETA_LO", "THETA_HI", "SCALE", "SIGMA_1", "AMPL_2", "SIGMA_2", "AMPL_3", "SIGMA_3", ] units = ["TeV", "TeV", "deg", "deg", "", "deg", "", "deg", "", "deg"] data = [ self.energy_lo, self.energy_hi, self.theta, self.theta, self.norms[0], self.sigmas[0], self.norms[1], self.sigmas[1], self.norms[2], self.sigmas[2], ] table = Table() for name_, data_, unit_ in zip(names, data, units): table[name_] = [data_] table[name_].unit = unit_ # Create hdu and hdu list hdu = fits.BinTableHDU(table) hdu.header["LO_THRES"] = self.energy_thresh_lo.value hdu.header["HI_THRES"] = self.energy_thresh_hi.value return fits.HDUList([fits.PrimaryHDU(), hdu]) def write(self, filename, *args, **kwargs): """Write PSF to FITS file. Calls `~astropy.io.fits.HDUList.writeto`, forwarding all arguments. """ self.to_fits().writeto(filename, *args, **kwargs) def psf_at_energy_and_theta(self, energy, theta): """ Get `~gammapy.image.models.MultiGauss2D` model for given energy and theta. No interpolation is used. Parameters ---------- energy : `~astropy.units.Quantity` Energy at which a PSF is requested. theta : `~astropy.coordinates.Angle` Offset angle at which a PSF is requested. Returns ------- psf : `~gammapy.morphology.MultiGauss2D` Multigauss PSF object. """ energy = Energy(energy) theta = Quantity(theta) pars = {} for name, interp_norm in zip(["scale", "A_2", "A_3"], self._interp_norms): pars[name] = interp_norm((theta, energy)) for idx, interp_sigma in enumerate(self._interp_sigmas): pars["sigma_{}".format(idx + 1)] = interp_sigma((theta, energy)) psf = HESSMultiGaussPSF(pars) return psf.to_MultiGauss2D(normalize=True) def containment_radius(self, energy, theta, fraction=0.68): """Compute containment for all energy and theta values""" # This is a false positive from pylint # See https://github.com/PyCQA/pylint/issues/2435 energies = Energy(energy).flatten() # pylint:disable=assignment-from-no-return thetas = Angle(theta).flatten() radius = np.empty((theta.size, energy.size)) for idx, energy in enumerate(energies): for jdx, theta in enumerate(thetas): try: psf = self.psf_at_energy_and_theta(energy, theta) radius[jdx, idx] = psf.containment_radius(fraction) except ValueError: log.debug( "Computing containment failed for E = {:.2f}" " and Theta={:.2f}".format(energy, theta) ) log.debug("Sigmas: {} Norms: {}".format(psf.sigmas, psf.norms)) radius[jdx, idx] = np.nan return Angle(radius, "deg") def plot_containment( self, fraction=0.68, ax=None, show_safe_energy=False, add_cbar=True, **kwargs ): """ Plot containment image with energy and theta axes. Parameters ---------- fraction : float Containment fraction between 0 and 1. add_cbar : bool Add a colorbar """ import matplotlib.pyplot as plt ax = plt.gca() if ax is None else ax energy = self.energy_hi offset = self.theta # Set up and compute data containment = self.containment_radius(energy, offset, fraction) # plotting defaults kwargs.setdefault("cmap", "GnBu") kwargs.setdefault("vmin", np.nanmin(containment.value)) kwargs.setdefault("vmax", np.nanmax(containment.value)) # Plotting x = energy.value y = offset.value caxes = ax.pcolormesh(x, y, containment.value, **kwargs) # Axes labels and ticks, colobar ax.semilogx() ax.set_ylabel("Offset ({unit})".format(unit=offset.unit)) ax.set_xlabel("Energy ({unit})".format(unit=energy.unit)) ax.set_xlim(x.min(), x.max()) ax.set_ylim(y.min(), y.max()) if show_safe_energy: self._plot_safe_energy_range(ax) if add_cbar: label = "Containment radius R{:.0f} ({})".format( 100 * fraction, containment.unit ) ax.figure.colorbar(caxes, ax=ax, label=label) return ax def _plot_safe_energy_range(self, ax): """add safe energy range lines to the plot""" esafe = self.energy_thresh_lo omin = self.offset.value.min() omax = self.offset.value.max() ax.hlines(y=esafe.value, xmin=omin, xmax=omax) label = "Safe energy threshold: {:3.2f}".format(esafe) ax.text(x=0.1, y=0.9 * esafe.value, s=label, va="top") def plot_containment_vs_energy( self, fractions=[0.68, 0.95], thetas=Angle([0, 1], "deg"), ax=None, **kwargs ): """Plot containment fraction as a function of energy. """ import matplotlib.pyplot as plt ax = plt.gca() if ax is None else ax energy = Energy.equal_log_spacing(self.energy_lo[0], self.energy_hi[-1], 100) for theta in thetas: for fraction in fractions: radius = self.containment_radius(energy, theta, fraction).squeeze() label = "{} deg, {:.1f}%".format(theta.deg, 100 * fraction) ax.plot(energy.value, radius.value, label=label) ax.semilogx() ax.legend(loc="best") ax.set_xlabel("Energy (TeV)") ax.set_ylabel("Containment radius (deg)") def peek(self, figsize=(15, 5)): """Quick-look summary plots.""" import matplotlib.pyplot as plt fig, axes = plt.subplots(nrows=1, ncols=3, figsize=figsize) self.plot_containment(fraction=0.68, ax=axes[0]) self.plot_containment(fraction=0.95, ax=axes[1]) self.plot_containment_vs_energy(ax=axes[2]) # TODO: implement this plot # psf = self.psf_at_energy_and_theta(energy='1 TeV', theta='1 deg') # psf.plot_components(ax=axes[2]) plt.tight_layout() def info( self, fractions=[0.68, 0.95], energies=Quantity([1.0, 10.0], "TeV"), thetas=Quantity([0.0], "deg"), ): """ Print PSF summary info. The containment radius for given fraction, energies and thetas is computed and printed on the command line. Parameters ---------- fractions : list Containment fraction to compute containment radius for. energies : `~astropy.units.Quantity` Energies to compute containment radius for. thetas : `~astropy.units.Quantity` Thetas to compute containment radius for. Returns ------- ss : string Formatted string containing the summary info. """ ss = "\nSummary PSF info\n" ss += "----------------\n" # Summarise data members ss += array_stats_str(self.theta.to("deg"), "Theta") ss += array_stats_str(self.energy_hi, "Energy hi") ss += array_stats_str(self.energy_lo, "Energy lo") ss += "Safe energy threshold lo: {:6.3f}\n".format(self.energy_thresh_lo) ss += "Safe energy threshold hi: {:6.3f}\n".format(self.energy_thresh_hi) for fraction in fractions: containment = self.containment_radius(energies, thetas, fraction) for i, energy in enumerate(energies): for j, theta in enumerate(thetas): radius = containment[j, i] ss += ( "{:2.0f}% containment radius at theta = {} and " "E = {:4.1f}: {:5.8f}\n" "".format(100 * fraction, theta, energy, radius) ) return ss def to_energy_dependent_table_psf(self, theta=None, rad=None, exposure=None): """ Convert triple Gaussian PSF ot table PSF. Parameters ---------- theta : `~astropy.coordinates.Angle` Offset in the field of view. Default theta = 0 deg rad : `~astropy.coordinates.Angle` Offset from PSF center used for evaluating the PSF on a grid. Default offset = [0, 0.005, ..., 1.495, 1.5] deg. exposure : `~astropy.units.Quantity` Energy dependent exposure. Should be in units equivalent to 'cm^2 s'. Default exposure = 1. Returns ------- tabe_psf : `~gammapy.irf.EnergyDependentTablePSF` Instance of `EnergyDependentTablePSF`. """ # Convert energies to log center energies = self.energy # Defaults and input handling if theta is None: theta = Angle(0, "deg") else: theta = Angle(theta) if rad is None: rad = Angle(np.arange(0, 1.5, 0.005), "deg") else: rad = Angle(rad).to("deg") psf_value = Quantity(np.zeros((energies.size, rad.size)), "deg^-2") for idx, energy in enumerate(energies): psf_gauss = self.psf_at_energy_and_theta(energy, theta) psf_value[idx] = Quantity(psf_gauss(rad), "deg^-2") return EnergyDependentTablePSF( energy=energies, rad=rad, exposure=exposure, psf_value=psf_value ) def to_psf3d(self, rad): """Create a PSF3D from an analytical PSF. Parameters ---------- rad : `~astropy.units.Quantity` or `~astropy.coordinates.Angle` the array of position errors (rad) on which the PSF3D will be defined Returns ------- psf3d : `~gammapy.irf.PSF3D` the PSF3D. It will be defined on the same energy and offset values than the input psf. """ offsets = self.theta energy = self.energy energy_lo = self.energy_lo energy_hi = self.energy_hi rad_lo = rad[:-1] rad_hi = rad[1:] psf_values = np.zeros( (rad_lo.shape[0], offsets.shape[0], energy_lo.shape[0]) ) * Unit("sr-1") for i, offset in enumerate(offsets): psftable = self.to_energy_dependent_table_psf(offset) psf_values[:, i, :] = psftable.evaluate(energy, 0.5 * (rad_lo + rad_hi)).T return PSF3D( energy_lo, energy_hi, offsets, rad_lo, rad_hi, psf_values, self.energy_thresh_lo, self.energy_thresh_hi, )
def _write_xspec_script(source: BaseSource, spec_storage_key: str, model: str, abund_table: str, fit_method: str, specs: str, lo_en: Quantity, hi_en: Quantity, par_names: str, par_values: str, linking: str, freezing: str, par_fit_stat: float, lum_low_lims: str, lum_upp_lims: str, lum_conf: float, redshift: float, pre_check: bool, check_par_names: str, check_par_lo_lims: str, check_par_hi_lims: str, check_par_err_lims: str, norm_scale: bool) -> Tuple[str, str]: """ This writes out a configured XSPEC script, and is common to all fit functions. :param BaseSource source: The source for which an XSPEC script is being created :param str spec_storage_key: The storage key that the spectra that have been included in the current fit are stored under. :param str model: The model being fitted to the data. :param str abund_table: The chosen abundance table for XSPEC to use. :param str fit_method: Which fit method should XSPEC use to fit the model to data. :param str specs: A string containing the paths to all spectra to be fitted. :param Quantity lo_en: The lower energy limit for the data to be fitted. :param Quantity hi_en: The upper energy limit for the data to be fitted. :param str par_names: A string containing the names of the model parameters. :param str par_values: A string containing the start values of the model parameters. :param str linking: A string containing the linking settings for the model. :param str freezing: A string containing the freezing settings for the model. :param float par_fit_stat: The delta fit statistic for the XSPEC 'error' command. :param str lum_low_lims: A string containing the lower energy limits for the luminosity measurements. :param str lum_upp_lims: A string containing the upper energy limits for the luminosity measurements. :param float lum_conf: The confidence level for XSPEC luminosity measurements. :param float redshift: The redshift of the object. :param bool pre_check: Flag indicating whether a pre-check of the quality of the input spectra should be performed. :param str check_par_names: A string representing a TCL list of model parameter names that checks should be performed on. :param str check_par_lo_lims: A string representing a TCL list of allowed lower limits for the check_par_names parameter entries. :param str check_par_hi_lims: A string representing a TCL list of allowed upper limits for the check_par_names parameter entries. :param str check_par_err_lims: A string representing a TCL list of allowed upper limits for the parameter uncertainties. :param bool norm_scale: Is there an extra constant designed to account for the differences in normalisation you can get from different observations of a cluster. :return: The paths to the output file and the script file. :rtype: Tuple[str, str] """ # Read in the template file for the XSPEC script. with open(BASE_XSPEC_SCRIPT, 'r') as x_script: script = x_script.read() # There has to be a directory to write this xspec script to, as well as somewhere for the fit output # to be stored dest_dir = OUTPUT + "XSPEC/" + source.name + "/" if not os.path.exists(dest_dir): os.makedirs(dest_dir) # Defining where the output summary file of the fit is written out_file = dest_dir + source.name + "_" + spec_storage_key + "_" + model script_file = dest_dir + source.name + "_" + spec_storage_key + "_" + model + ".xcm" # The template is filled out here, taking everything we have generated and everything the user # passed in. The result is an XSPEC script that can be run as is. script = script.format(xsp=XGA_EXTRACT, ab=abund_table, md=fit_method, H0=source.cosmo.H0.value, q0=0., lamb0=source.cosmo.Ode0, sp=specs, lo_cut=lo_en.to("keV").value, hi_cut=hi_en.to("keV").value, m=model, pn=par_names, pv=par_values, lk=linking, fr=freezing, el=par_fit_stat, lll=lum_low_lims, lul=lum_upp_lims, of=out_file, redshift=redshift, lel=lum_conf, check=pre_check, cps=check_par_names, cpsl=check_par_lo_lims, cpsh=check_par_hi_lims, cpse=check_par_err_lims, ns=norm_scale) # Write out the filled-in template to its destination with open(script_file, 'w') as xcm: xcm.write(script) return out_file, script_file
def make_test_bg_cube_model(detx_range=Angle([-10., 10.], 'deg'), ndetx_bins=24, dety_range=Angle([-10., 10.], 'deg'), ndety_bins=24, energy_band=Quantity([0.01, 100.], 'TeV'), nenergy_bins=14, altitude=Angle(70., 'deg'), sigma=Angle(5., 'deg'), spectral_index=2.7, apply_mask=False, do_not_force_mev_units=False): """Make a test bg cube model. The background counts cube is created following a 2D symmetric gaussian model for the spatial coordinates (X, Y) and a power-law in energy. The gaussian width varies in energy from sigma/2 to sigma. The power-law slope in log-log representation is given by the spectral_index parameter. The norm depends linearly on the livetime and on the altitude angle of the observation. It is possible to mask 1/4th of the image (for **x > x_center** and **y > y_center**). Useful for testing coordinate rotations. Per default units of *1 / (MeV sr s)* for the bg rate are enforced, unless *do_not_force_mev_units* is set. This is in agreement to the convention applied in `~gammapy.background.make_bg_cube_model`. This method is useful for instance to produce true (simulated) background cube models to compare to the reconstructed ones produced with `~gammapy.background.make_bg_cube_model`. For details on how to do this, please refer to :ref:`background_make_background_models_datasets_for_testing`. Parameters ---------- detx_range : `~astropy.coordinates.Angle`, optional X coordinate range (min, max). ndetx_bins : int, optional Number of (linear) bins in X coordinate. dety_range : `~astropy.coordinates.Angle`, optional Y coordinate range (min, max). ndety_bins : int, optional Number of (linear) bins in Y coordinate. energy_band : `~astropy.units.Quantity`, optional Energy range (min, max). nenergy_bins : int, optional Number of (logarithmic) bins in energy. altitude : `~astropy.coordinates.Angle`, optional observation altitude angle for the model. sigma : `~astropy.coordinates.Angle`, optional Width of the gaussian model used for the spatial coordinates. spectral_index : float, optional Index for the power-law model used for the energy coordinate. apply_mask : bool, optional If set, 1/4th of the image is masked (for **x > x_center** and **y > y_center**). do_not_force_mev_units : bool, optional Set to ``True`` to use the same energy units as the energy binning for the bg rate. Returns ------- bg_cube_model : `~gammapy.background.FOVCubeBackgroundModel` Bacground cube model. """ from ..background import FOVCubeBackgroundModel # spatial bins (linear) delta_x = (detx_range[1] - detx_range[0]) / ndetx_bins detx_bin_edges = np.arange(ndetx_bins + 1) * delta_x + detx_range[0] delta_y = (dety_range[1] - dety_range[0]) / ndety_bins dety_bin_edges = np.arange(ndety_bins + 1) * delta_y + dety_range[0] # energy bins (logarithmic) log_delta_energy = (np.log(energy_band[1].value) - np.log(energy_band[0].value)) / nenergy_bins energy_bin_edges = np.exp( np.arange(nenergy_bins + 1) * log_delta_energy + np.log(energy_band[0].value)) energy_bin_edges = Quantity(energy_bin_edges, energy_band[0].unit) # TODO: this function should be reviewed/re-written, when # the following PR is completed: # https://github.com/gammapy/gammapy/pull/290 # define empty bg cube model and set bins bg_cube_model = FOVCubeBackgroundModel.set_cube_binning( detx_edges=detx_bin_edges, dety_edges=dety_bin_edges, energy_edges=energy_bin_edges) # counts # define coordinate grids for the calculations det_bin_centers = bg_cube_model.counts_cube.image_bin_centers energy_bin_centers = bg_cube_model.counts_cube.energy_edges.log_centers energy_points, dety_points, detx_points = np.meshgrid(energy_bin_centers, det_bin_centers[1], det_bin_centers[0], indexing='ij') E_0 = Quantity(1., 'TeV') # reference energy for the model # norm of the model # taking as reference for now a dummy value of 1 # it is linearly dependent on the zenith angle (90 deg - altitude) # it is norm_max at alt = 90 deg and norm_max/2 at alt = 0 deg norm_max = Quantity(1, '') alt_min = Angle(0., 'deg') alt_max = Angle(90., 'deg') slope = (norm_max - norm_max / 2) / (alt_max - alt_min) free_term = norm_max / 2 - slope * alt_min norm = altitude * slope + free_term # define E dependent sigma # it is defined via a PL, in order to be log-linear # it is equal to the parameter sigma at E max # and sigma/2. at E min sigma_min = sigma / 2. # at E min sigma_max = sigma # at E max s_index = np.log(sigma_max / sigma_min) s_index /= np.log(energy_bin_edges[-1] / energy_bin_edges[0]) s_norm = sigma_min * ((energy_bin_edges[0] / E_0)**-s_index) sigma = s_norm * ((energy_points / E_0)**s_index) # calculate counts gaussian = np.exp(-((detx_points)**2 + (dety_points)**2) / sigma**2) powerlaw = (energy_points / E_0)**-spectral_index counts = norm * gaussian * powerlaw bg_cube_model.counts_cube.data = Quantity(counts, '') # livetime # taking as reference for now a dummy value of 1 s livetime = Quantity(1., 'second') bg_cube_model.livetime_cube.data += livetime # background bg_cube_model.background_cube.data = bg_cube_model.counts_cube.data.copy() bg_cube_model.background_cube.data /= bg_cube_model.livetime_cube.data bg_cube_model.background_cube.data /= bg_cube_model.background_cube.bin_volume # bg_cube_model.background_cube.set_zero_level() if not do_not_force_mev_units: # use units of 1 / (MeV sr s) for the bg rate bg_rate = bg_cube_model.background_cube.data.to('1 / (MeV sr s)') bg_cube_model.background_cube.data = bg_rate # apply mask if requested if apply_mask: # find central coordinate detx_center = (detx_range[1] + detx_range[0]) / 2. dety_center = (dety_range[1] + dety_range[0]) / 2. mask = (detx_points <= detx_center) & (dety_points <= dety_center) bg_cube_model.counts_cube.data *= mask bg_cube_model.livetime_cube.data *= mask bg_cube_model.background_cube.data *= mask return bg_cube_model
def single_temp_apec(sources: Union[BaseSource, BaseSample], outer_radius: Union[str, Quantity], inner_radius: Union[str, Quantity] = Quantity(0, 'arcsec'), start_temp: Quantity = Quantity(3.0, "keV"), start_met: float = 0.3, lum_en: Quantity = Quantity([[0.5, 2.0], [0.01, 100.0]], "keV"), freeze_nh: bool = True, freeze_met: bool = True, lo_en: Quantity = Quantity(0.3, "keV"), hi_en: Quantity = Quantity(7.9, "keV"), par_fit_stat: float = 1., lum_conf: float = 68., abund_table: str = "angr", fit_method: str = "leven", group_spec: bool = True, min_counts: int = 5, min_sn: float = None, over_sample: float = None, one_rmf: bool = True, num_cores: int = NUM_CORES, spectrum_checking: bool = True, timeout: Quantity = Quantity(1, 'hr')): """ This is a convenience function for fitting an absorbed single temperature apec model(constant*tbabs*apec) to an object. It would be possible to do the exact same fit using the custom_model function, but as it will be a very common fit a dedicated function is in order. If there are no existing spectra with the passed settings, then they will be generated automatically. If the spectrum checking step of the XSPEC fit is enabled (using the boolean flag spectrum_checking), then each individual spectrum available for a given source will be fitted, and if the measured temperature is less than or equal to 0.01keV, or greater than 20keV, or the temperature uncertainty is greater than 15keV, then that spectrum will be rejected and not included in the final fit. Spectrum checking also involves rejecting any spectra with fewer than 10 noticed channels. :param List[BaseSource] sources: A single source object, or a sample of sources. :param str/Quantity outer_radius: The name or value of the outer radius of the region that the desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster, or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any inner radius will be ignored. If you are fitting for multiple sources then you can also pass a Quantity with one entry per source. :param str/Quantity inner_radius: The name or value of the outer radius of the region that the desired spectrum covers (for instance 'r200' would be acceptable for a GalaxyCluster, or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any inner radius will be ignored. By default this is zero arcseconds, resulting in a circular spectrum. If you are fitting for multiple sources then you can also pass a Quantity with one entry per source. :param Quantity start_temp: The initial temperature for the fit. :param start_met: The initial metallicity for the fit (in ZSun). :param Quantity lum_en: Energy bands in which to measure luminosity. :param bool freeze_nh: Whether the hydrogen column density should be frozen. :param bool freeze_met: Whether the metallicity parameter in the fit should be frozen. :param Quantity lo_en: The lower energy limit for the data to be fitted. :param Quantity hi_en: The upper energy limit for the data to be fitted. :param float par_fit_stat: The delta fit statistic for the XSPEC 'error' command, default is 1.0 which should be equivelant to 1σ errors if I've understood (https://heasarc.gsfc.nasa.gov/xanadu/xspec/manual/XSerror.html) correctly. :param float lum_conf: The confidence level for XSPEC luminosity measurements. :param str abund_table: The abundance table to use for the fit. :param str fit_method: The XSPEC fit method to use. :param bool group_spec: A boolean flag that sets whether generated spectra are grouped or not. :param float min_counts: If generating a grouped spectrum, this is the minimum number of counts per channel. To disable minimum counts set this parameter to None. :param float min_sn: If generating a grouped spectrum, this is the minimum signal to noise in each channel. To disable minimum signal to noise set this parameter to None. :param float over_sample: The minimum energy resolution for each group, set to None to disable. e.g. if over_sample=3 then the minimum width of a group is 1/3 of the resolution FWHM at that energy. :param bool one_rmf: This flag tells the method whether it should only generate one RMF for a particular ObsID-instrument combination - this is much faster in some circumstances, however the RMF does depend slightly on position on the detector. :param int num_cores: The number of cores to use (if running locally), default is set to 90% of available. :param bool spectrum_checking: Should the spectrum checking step of the XSPEC fit (where each spectrum is fit individually and tested to see whether it will contribute to the simultaneous fit) be activated? :param Quantity timeout: The amount of time each individual fit is allowed to run for, the default is one hour. Please note that this is not a timeout for the entire fitting process, but a timeout to individual source fits. """ sources, inn_rad_vals, out_rad_vals = _pregen_spectra( sources, outer_radius, inner_radius, group_spec, min_counts, min_sn, over_sample, one_rmf, num_cores) sources = _check_inputs(sources, lum_en, lo_en, hi_en, fit_method, abund_table, timeout) # This function is for a set model, absorbed apec, so I can hard code all of this stuff. # These will be inserted into the general XSPEC script template, so lists of parameters need to be in the form # of TCL lists. model = "constant*tbabs*apec" par_names = "{factor nH kT Abundanc Redshift norm}" lum_low_lims = "{" + " ".join(lum_en[:, 0].to("keV").value.astype(str)) + "}" lum_upp_lims = "{" + " ".join(lum_en[:, 1].to("keV").value.astype(str)) + "}" script_paths = [] outfile_paths = [] src_inds = [] # This function supports passing multiple sources, so we have to setup a script for all of them. for src_ind, source in enumerate(sources): # Find matching spectrum objects associated with the current source spec_objs = source.get_spectra(out_rad_vals[src_ind], inner_radius=inn_rad_vals[src_ind], group_spec=group_spec, min_counts=min_counts, min_sn=min_sn, over_sample=over_sample) # This is because many other parts of this function assume that spec_objs is iterable, and in the case of # a cluster with only a single valid instrument for a single valid observation this may not be the case if isinstance(spec_objs, Spectrum): spec_objs = [spec_objs] # Obviously we can't do a fit if there are no spectra, so throw an error if that's the case if len(spec_objs) == 0: raise NoProductAvailableError( "There are no matching spectra for {s} object, you " "need to generate them first!".format(s=source.name)) # Turn spectra paths into TCL style list for substitution into template specs = "{" + " ".join([spec.path for spec in spec_objs]) + "}" # For this model, we have to know the redshift of the source. if source.redshift is None: raise ValueError( "You cannot supply a source without a redshift to this model.") # Whatever start temperature is passed gets converted to keV, this will be put in the template t = start_temp.to("keV", equivalencies=u.temperature_energy()).value # Another TCL list, this time of the parameter start values for this model. par_values = "{{{0} {1} {2} {3} {4} {5}}}".format( 1., source.nH.to("10^22 cm^-2").value, t, start_met, source.redshift, 1.) # Set up the TCL list that defines which parameters are frozen, dependant on user input if freeze_nh and freeze_met: freezing = "{F T F T T F}" elif not freeze_nh and freeze_met: freezing = "{F F F T T F}" elif freeze_nh and not freeze_met: freezing = "{F T F F T F}" elif not freeze_nh and not freeze_met: freezing = "{F F F F T F}" # Set up the TCL list that defines which parameters are linked across different spectra, only the # multiplicative constant that accounts for variation in normalisation over different observations is not # linked linking = "{F T T T T T}" # If the user wants the spectrum cleaning step to be run, then we have to setup some acceptable # limits. For this function they will be hardcoded, for simplicities sake, and we're only going to # check the temperature, as its the main thing we're fitting for with constant*tbabs*apec if spectrum_checking: check_list = "{kT}" check_lo_lims = "{0.01}" check_hi_lims = "{20}" check_err_lims = "{15}" else: check_list = "{}" check_lo_lims = "{}" check_hi_lims = "{}" check_err_lims = "{}" out_file, script_file = _write_xspec_script( source, spec_objs[0].storage_key, model, abund_table, fit_method, specs, lo_en, hi_en, par_names, par_values, linking, freezing, par_fit_stat, lum_low_lims, lum_upp_lims, lum_conf, source.redshift, spectrum_checking, check_list, check_lo_lims, check_hi_lims, check_err_lims, True) # If the fit has already been performed we do not wish to perform it again try: res = source.get_results(out_rad_vals[src_ind], model, inn_rad_vals[src_ind], 'kT', group_spec, min_counts, min_sn, over_sample) except ModelNotAssociatedError: script_paths.append(script_file) outfile_paths.append(out_file) src_inds.append(src_ind) run_type = "fit" return script_paths, outfile_paths, num_cores, run_type, src_inds, None, timeout
def mDM(self, mDM): mDM_vals = self.table['mDM'].data mDM_ = Quantity(mDM).to('GeV').value interp_idx = np.argmin(np.abs(mDM_vals - mDM_)) self._mDM = Quantity(mDM_vals[interp_idx], 'GeV')
def lumi(t): t = Quantity(t, "s") return pulsar.luminosity_spindown(t).value
def tr_add_unit(value, unitname): return Quantity(value, unitname)
def test_Pulsar_period(): """Test pulsar period""" reference = Quantity([0.10000001, 0.10000123, 0.10012331, 0.11270709], "s") assert_quantity_allclose(pulsar.period(time), reference)
def _dens_setup( sources: Union[GalaxyCluster, ClusterSample], outer_radius: Union[str, Quantity], inner_radius: Union[str, Quantity], abund_table: str, lo_en: Quantity, hi_en: Quantity, group_spec: bool = True, min_counts: int = 5, min_sn: float = None, over_sample: float = None, obs_id: Union[str, list] = None, inst: Union[str, list] = None, conv_temp: Quantity = None, conv_outer_radius: Quantity = "r500", num_cores: int = NUM_CORES ) -> Tuple[Union[ClusterSample, List], List[Quantity], list, list]: """ An internal function which exists because all the density profile methods that I have planned need the same product checking and setup steps. This function checks that all necessary spectra/fits have been generated/run, then uses them to calculate the conversion factors from count-rate/volume to squared hydrogen number density. :param Union[GalaxyCluster, ClusterSample] sources: The source objects/sample object for which the density profile is being found. :param str/Quantity outer_radius: The name or value of the outer radius of the spectra that should be used to calculate conversion factors (for instance 'r200' would be acceptable for a GalaxyCluster, or Quantity(1000, 'kpc')). If 'region' is chosen (to use the regions in region files), then any inner radius will be ignored. :param str/Quantity inner_radius: The name or value of the inner radius of the spectra that should be used to calculate conversion factors (for instance 'r500' would be acceptable for a GalaxyCluster, or Quantity(300, 'kpc')). By default this is zero arcseconds, resulting in a circular spectrum. :param str abund_table: Which abundance table should be used for the XSPEC fit, FakeIt run, and for the electron/hydrogen number density ratio. :param Quantity lo_en: The lower energy limit of the combined ratemap used to calculate density. :param Quantity hi_en: The upper energy limit of the combined ratemap used to calculate density. :param bool group_spec: Whether the spectra that were fitted for the desired result were grouped. :param float min_counts: The minimum counts per channel, if the spectra that were fitted for the desired result were grouped by minimum counts. :param float min_sn: The minimum signal to noise per channel, if the spectra that were fitted for the desired result were grouped by minimum signal to noise. :param float over_sample: The level of oversampling applied on the spectra that were fitted. :param str/list obs_id: A specific ObsID(s) to measure the density from. This should be a string if a single source is being analysed, and a list of ObsIDs the same length as the number of sources otherwise. The default is None, in which case the combined data will be used to measure the density profile. :param str/list inst: A specific instrument(s) to measure the density from. This can either be passed as a single string (e.g. 'pn') if only one source is being analysed, or the same instrument should be used for every source in a sample, or a list of strings if different instruments are required for each source. The default is None, in which case the combined data will be used to measure the density profile. :param Quantity conv_temp: If set this will override XGA measured temperatures within the conv_outer_radius, and the fakeit run to calculate the normalisation conversion factor will use these temperatures. The quantity should have an entry for each cluster being analysed. Default is None. :param str/Quantity conv_outer_radius: The outer radius within which to generate spectra and measure temperatures for the conversion factor calculation, default is 'r500'. An astropy quantity may also be passed, with either a single value or an entry for each cluster being analysed. :param int num_cores: The number of cores that the evselect call and XSPEC functions are allowed to use. :return: The source object(s)/sample that was passed in, an array of the calculated conversion factors to take the count-rate/volume to a number density of hydrogen, the parsed obs_id variable, and the parsed inst variable. :rtype: Tuple[Union[ClusterSample, List], List[Quantity], list, list] """ # If its a single source I shove it in a list so I can just iterate over the sources parameter # like I do when its a Sample object if isinstance(sources, BaseSource): sources = [sources] # Perform some checks on the ObsID and instrument parameters to make sure that they are in the correct # format if they have been set. We don't need to check that the ObsIDs are associated with the sources # here, because that will happen when the ratemaps are retrieved from the source objects. if all([obs_id is not None, inst is not None]): if isinstance(obs_id, str): obs_id = [obs_id] if isinstance(inst, str): inst = [inst] if len(obs_id) != len(sources): raise ValueError( "If you set the obs_id argument there must be one entry per source being analysed." ) if len(inst) != len(sources) and len(inst) != 1: raise ValueError( "The value passed for inst must either be a single instrument name, or a list " "of instruments the same length as the number of sources being analysed." ) elif all([obs_id is None, inst is None]): obs_id = [None] * len(sources) inst = [None] * len(sources) else: raise ValueError( "If a value is supplied for obs_id, then a value must be supplied for inst as well, and " "vice versa.") if not all([type(src) == GalaxyCluster for src in sources]): raise TypeError( "Only GalaxyCluster sources can be passed to cluster_density_profile." ) # Triggers an exception if the abundance table name passed isn't recognised if abund_table not in ABUND_TABLES: ab_list = ", ".join(ABUND_TABLES) raise ValueError( "{0} is not in the accepted list of abundance tables; {1}".format( abund_table, ab_list)) # This check will eventually become obselete, but I haven't yet implemented electron to proton ratios for # all allowed abundance tables - so this just checks whether the chosen table has an ratio associated. try: e_to_p_ratio = NHC[abund_table] except KeyError: raise NotImplementedError( "That is an acceptable abundance table, but I haven't added the conversion factor " "to the dictionary yet") if conv_temp is not None and not conv_temp.isscalar and len( conv_temp) != len(sources): raise ValueError( "If multiple there are multiple entries in conv_temp, then there must be the same number" " of entries as there are sources being analysed.") elif conv_temp is not None: temps = conv_temp else: # Check that the spectra we will be relying on for conversion calculation have been fitted, calling # this function will also make sure that they are generated single_temp_apec(sources, conv_outer_radius, inner_radius, abund_table=abund_table, num_cores=num_cores, group_spec=group_spec, min_counts=min_counts, min_sn=min_sn, over_sample=over_sample) # Then we need to grab the temperatures and pass them through to the cluster conversion factor # calculator - this may well change as I intend to let cluster_cr_conv grab temperatures for # itself at some point temp_temps = [] for src in sources: try: # A temporary temperature variable temp_temp = src.get_temperature(conv_outer_radius, "constant*tbabs*apec", inner_radius, group_spec, min_counts, min_sn, over_sample)[0] except (ModelNotAssociatedError, ParameterNotAssociatedError): warn( "{s}'s temperature fit is not valid, so I am defaulting to a temperature of " "3keV".format(s=src.name)) temp_temp = Quantity(3, 'keV') temp_temps.append(temp_temp.value) temps = Quantity(temp_temps, 'keV') # This call actually does the fakeit calculation of the conversion factors, then stores them in the # XGA Spectrum objects cluster_cr_conv(sources, conv_outer_radius, inner_radius, temps, abund_table=abund_table, num_cores=num_cores, group_spec=group_spec, min_counts=min_counts, min_sn=min_sn, over_sample=over_sample) # This where the combined conversion factor that takes a count-rate/volume to a squared number density # of hydrogen to_dens_convs = [] # These are from the distance and redshift, also the normalising 10^-14 (see my paper for # more of an explanation) for src_ind, src in enumerate(sources): src: GalaxyCluster # Both the angular_diameter_distance and redshift are guaranteed to be present here because redshift # is REQUIRED to define GalaxyCluster objects factor = ((4 * e_to_p_ratio * np.pi * (src.angular_diameter_distance.to("cm") * (1 + src.redshift))**2) / 10**-14) total_factor = factor * src.norm_conv_factor( conv_outer_radius, lo_en, hi_en, inner_radius, group_spec, min_counts, min_sn, over_sample, obs_id[src_ind], inst[src_ind]) to_dens_convs.append(total_factor) return sources, to_dens_convs, obs_id, inst
# Licensed under a 3-clause BSD style license - see LICENSE.rst from numpy.testing import assert_allclose from astropy.units import Quantity from astropy.table import Table from ....utils.testing import assert_quantity_allclose from ...source import Pulsar, SimplePulsar pulsar = Pulsar() time = Quantity([1e2, 1e4, 1e6, 1e8], "yr") def get_atnf_catalog_sample(): data = """ NUM NAME Gl Gb P0 P1 AGE BSURF EDOT 1 J0006+1834 108.172 -42.985 0.693748 2.10e-15 5.24e+06 1.22e+12 2.48e+32 2 J0007+7303 119.660 10.463 0.315873 3.60e-13 1.39e+04 1.08e+13 4.51e+35 3 B0011+47 116.497 -14.631 1.240699 5.64e-16 3.48e+07 8.47e+11 1.17e+31 7 B0021-72E 305.883 -44.883 0.003536 9.85e-20 5.69e+08 5.97e+08 8.79e+34 8 B0021-72F 305.899 -44.892 0.002624 6.45e-20 6.44e+08 4.16e+08 1.41e+35 16 J0024-7204O 305.897 -44.889 0.002643 3.04e-20 1.38e+09 2.87e+08 6.49e+34 18 J0024-7204Q 305.877 -44.899 0.004033 3.40e-20 1.88e+09 3.75e+08 2.05e+34 21 J0024-7204T 305.890 -44.894 0.007588 2.94e-19 4.09e+08 1.51e+09 2.65e+34 22 J0024-7204U 305.890 -44.905 0.004343 9.52e-20 7.23e+08 6.51e+08 4.59e+34 28 J0026+6320 120.176 0.593 0.318358 1.50e-16 3.36e+07 2.21e+11 1.84e+32 """ return Table.read(data, format="ascii") def test_SimplePulsar_atnf(): """Test functions against ATNF pulsar catalog values""" atnf = get_atnf_catalog_sample()
def _onion_peel_data(sources: Union[GalaxyCluster, ClusterSample], outer_radius: Union[str, Quantity] = "r500", num_dens: bool = True, use_peak: bool = True, pix_step: int = 1, min_snr: Union[int, float] = 0.0, abund_table: str = "angr", lo_en: Quantity = Quantity(0.5, 'keV'), hi_en: Quantity = Quantity(2.0, 'keV'), psf_corr: bool = True, psf_model: str = "ELLBETA", psf_bins: int = 4, psf_algo: str = "rl", psf_iter: int = 15, num_samples: int = 10000, group_spec: bool = True, min_counts: int = 5, min_sn: float = None, over_sample: float = None, obs_id: Union[str, list] = None, inst: Union[str, list] = None, conv_temp: Quantity = None, conv_outer_radius: Quantity = "r500", num_cores: int = NUM_CORES): raise NotImplementedError("This isn't finished at the moment") # Run the setup function, calculates the factors that translate 3D countrate to density # Also checks parameters and runs any spectra/fits that need running sources, conv_factors, obs_id, inst = _dens_setup( sources, outer_radius, Quantity(0, 'arcsec'), abund_table, lo_en, hi_en, group_spec, min_counts, min_sn, over_sample, obs_id, inst, conv_temp, conv_outer_radius, num_cores) # Calls the handy spectrum region setup function to make a predictable set of outer radius values out_rads = region_setup(sources, outer_radius, Quantity(0, 'arcsec'), False, '')[-1] final_dens_profs = [] # I need the ratio of electrons to protons here as well, so just fetch that for the current abundance table e_to_p_ratio = NHC[abund_table] with tqdm(desc="Generating density profiles based on onion-peeled data", total=len(sources)) as dens_onwards: for src_ind, src in enumerate(sources): sb_prof = _run_sb(src, out_rads[src_ind], use_peak, lo_en, hi_en, psf_corr, psf_model, psf_bins, psf_algo, psf_iter, pix_step, min_snr, obs_id[src_ind], inst[src_ind]) if sb_prof is None: final_dens_profs.append(None) continue else: src.update_products(sb_prof) rad_bounds = sb_prof.annulus_bounds.to("cm") vol_intersects = shell_ann_vol_intersect(rad_bounds, rad_bounds) print(vol_intersects.min()) plt.imshow(vol_intersects.value) plt.show() # Generating random normalisation profile realisations from DATA sb_reals = sb_prof.generate_data_realisations( num_samples) * sb_prof.areas # Using a loop here is ugly and relatively slow, but it should be okay transformed = [] for i in range(0, num_samples): transformed.append( np.linalg.inv(vol_intersects.T) @ sb_reals[i, :]) transformed = Quantity(transformed) print(np.percentile(transformed, 50, axis=0)) import sys sys.exit() # We convert the volume element to cm^3 now, this is the unit we expect for the density conversion transformed = transformed.to('ct/(s*cm^3)') num_dens_dist = np.sqrt( transformed * conv_factors[src_ind]) * (1 + e_to_p_ratio) print( np.where(np.isnan(np.sqrt(transformed * conv_factors[src_ind])))[0].shape) import sys sys.exit() med_num_dens = np.percentile(num_dens_dist, 50, axis=1) num_dens_err = np.std(num_dens_dist, axis=1) # Setting up the instrument and ObsID to pass into the density profile definition if obs_id[src_ind] is None: cur_inst = "combined" cur_obs = "combined" else: cur_inst = inst[src_ind] cur_obs = obs_id[src_ind] dens_rads = sb_prof.radii.copy() dens_rads_errs = sb_prof.radii_err.copy() dens_deg_rads = sb_prof.deg_radii.copy() print(np.where(np.isnan(transformed))) print(med_num_dens) try: # I now allow the user to decide if they want to generate number or mass density profiles using # this function, and here is where that distinction is made if num_dens: dens_prof = GasDensity3D(dens_rads.to("kpc"), med_num_dens, sb_prof.centre, src.name, cur_obs, cur_inst, 'onion', sb_prof, dens_rads_errs, num_dens_err, deg_radii=dens_deg_rads) else: # The mean molecular weight multiplied by the proton mass conv_mass = MEAN_MOL_WEIGHT * m_p dens_prof = GasDensity3D( dens_rads.to("kpc"), (med_num_dens * conv_mass).to('Msun/Mpc^3'), sb_prof.centre, src.name, cur_obs, cur_inst, 'onion', sb_prof, dens_rads_errs, (num_dens_err * conv_mass).to('Msun/Mpc^3'), deg_radii=dens_deg_rads) src.update_products(dens_prof) final_dens_profs.append(dens_prof) # If, for some reason, there are some inf/NaN values in any of the quantities passed to the GasDensity3D # declaration, this is where an error will be thrown except ValueError: final_dens_profs.append(None) warn( "One or more of the quantities passed to the init of {}'s density profile has a NaN or Inf value" " in it.".format(src.name)) dens_onwards.update(1) return final_dens_profs
def _power_law(E, N, k): E = Quantity(E, "TeV") E0 = Quantity(1, "TeV") N = Quantity(N, "m^-2 s^-1 TeV^-1 sr^-1") flux = N * (E / E0)**(-k) return flux
def __init__(self, nodes, name='Default', interpolation_mode='linear'): # Need this for subclassing (see BinnedDataAxis) if nodes is not None: self._data = Quantity(nodes) self.name = name self._interpolation_mode = interpolation_mode
def time_stop(self): """GTI end times (`~astropy.time.Time`).""" met = Quantity(self.table["STOP"].astype("float64"), "second") return self.time_ref + met
def make_test_eventlist(observation_table, obs_id, sigma=Angle(5., 'deg'), spectral_index=2.7, random_state='random-seed'): """ Make a test event list for a specified observation. The observation can be specified with an observation table object and the observation ID pointing to the correct observation in the table. For now, only a very rudimentary event list is generated, containing only detector X, Y coordinates (a.k.a. nominal system) and energy columns for the events. And the livetime of the observations stored in the header. The model used to simulate events is also very simple. Only dummy background is created (no signal). The background is created following a 2D symmetric gaussian model for the spatial coordinates (X, Y) and a power-law in energy. The gaussian width varies in energy from sigma/2 to sigma. The number of events generated depends linearly on the livetime and on the altitude angle of the observation. The model can be tuned via the sigma and spectral_index parameters. In addition, an effective area table is produced. For the moment only the low energy threshold is filled. See also :ref:`datasets_obssim`. Parameters ---------- observation_table : `~gammapy.data.ObservationTable` Observation table containing the observation to fake. obs_id : int Observation ID of the observation to fake inside the observation table. sigma : `~astropy.coordinates.Angle`, optional Width of the gaussian model used for the spatial coordinates. spectral_index : float, optional Index for the power-law model used for the energy coordinate. random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}, optional Defines random number generator initialisation. Passed to `~gammapy.utils.random.get_random_state`. Returns ------- event_list : `~gammapy.data.EventList` Event list. aeff_hdu : `~astropy.io.fits.BinTableHDU` Effective area table. """ from ..data import EventList random_state = get_random_state(random_state) # find obs row in obs table obs_ids = observation_table['OBS_ID'].data obs_index = np.where(obs_ids == obs_id) row = obs_index[0][0] # get observation information alt = Angle(observation_table['ALT'])[row] livetime = Quantity(observation_table['LIVETIME'])[row] # number of events to simulate # it is linearly dependent on the livetime, taking as reference # a trigger rate of 300 Hz # it is linearly dependent on the zenith angle (90 deg - altitude) # it is n_events_max at alt = 90 deg and n_events_max/2 at alt = 0 deg n_events_max = Quantity(300., 'Hz') * livetime alt_min = Angle(0., 'deg') alt_max = Angle(90., 'deg') slope = (n_events_max - n_events_max / 2) / (alt_max - alt_min) free_term = n_events_max / 2 - slope * alt_min n_events = alt * slope + free_term # simulate energy # the index of `~numpy.random.RandomState.power` has to be # positive defined, so it is necessary to translate the (0, 1) # interval of the random variable to (emax, e_min) in order to # have a decreasing power-law e_min = Quantity(0.1, 'TeV') e_max = Quantity(100., 'TeV') energy = sample_powerlaw(e_min.value, e_max.value, spectral_index, size=n_events, random_state=random_state) energy = Quantity(energy, 'TeV') E_0 = Quantity(1., 'TeV') # reference energy for the model # define E dependent sigma # it is defined via a PL, in order to be log-linear # it is equal to the parameter sigma at E max # and sigma/2. at E min sigma_min = sigma / 2. # at E min sigma_max = sigma # at E max s_index = np.log(sigma_max / sigma_min) s_index /= np.log(e_max / e_min) s_norm = sigma_min * ((e_min / E_0)**-s_index) sigma = s_norm * ((energy / E_0)**s_index) # simulate detx, dety detx = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events), 'deg') dety = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events), 'deg') # fill events in an event list event_list = EventList() event_list['DETX'] = detx event_list['DETY'] = dety event_list['ENERGY'] = energy # store important info in header event_list.meta['LIVETIME'] = livetime.to('second').value event_list.meta['EUNIT'] = str(energy.unit) # effective area table aeff_table = Table() # fill threshold, for now, a default 100 GeV will be set # independently of observation parameters energy_threshold = Quantity(0.1, 'TeV') aeff_table.meta['LO_THRES'] = energy_threshold.value aeff_table.meta['name'] = 'EFFECTIVE AREA' # convert to BinTableHDU and add necessary comment for the units aeff_hdu = table_to_fits_table(aeff_table) aeff_hdu.header.comments['LO_THRES'] = '[' + str( energy_threshold.unit) + ']' return event_list, aeff_hdu
__all__ = [ "CaseBattacharya1998", "FaucherKaspi2006", "Lorimer2006", "Paczynski1990", "YusifovKucuk2004", "YusifovKucuk2004B", "Exponential", "LogSpiral", "FaucherSpiral", "ValleeSpiral", "radial_distributions", ] # Simulation range used for random number drawing RMIN, RMAX = Quantity([0, 20], "kpc") ZMIN, ZMAX = Quantity([-0.5, 0.5], "kpc") class Paczynski1990(Fittable1DModel): """Radial distribution of the birth surface density of neutron stars - Paczynski 1990. .. math :: f(r) = A r_{exp}^{-2} \\exp \\left(-\\frac{r}{r_{exp}} \\right) Reference: http://adsabs.harvard.edu/abs/1990ApJ...348..485P (Formula (2)) Parameters ---------- amplitude : float See formula
class FaucherSpiral(LogSpiral): """Milky way spiral arm used in Faucher et al (2006). Reference: http://adsabs.harvard.edu/abs/2006ApJ...643..332F """ # Parameters k = Quantity([4.25, 4.25, 4.89, 4.89], "rad") r_0 = Quantity([3.48, 3.48, 4.9, 4.9], "kpc") theta_0 = Quantity([1.57, 4.71, 4.09, 0.95], "rad") spiralarms = np.array( ["Norma", "Carina Sagittarius", "Perseus", "Crux Scutum"]) def _blur(self, radius, theta, amount=0.07, random_state="random-seed"): """Blur the positions around the centroid of the spiralarm. The given positions are blurred by drawing a displacement in radius from a normal distribution, with sigma = amount * radius. And a direction theta from a uniform distribution in the interval [0, 2 * pi]. Parameters ---------- radius : `~astropy.units.Quantity` Radius coordinate theta : `~astropy.units.Quantity` Angle coordinate amount: float, optional Amount of blurring of the position, given as a fraction of `radius`. random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`} Defines random number generator initialisation. Passed to `~gammapy.utils.random.get_random_state`. """ random_state = get_random_state(random_state) dr = Quantity( abs(random_state.normal(0, amount * radius, radius.size)), "kpc") dtheta = Quantity(random_state.uniform(0, 2 * np.pi, radius.size), "rad") x, y = cartesian(radius, theta) dx, dy = cartesian(dr, dtheta) return polar(x + dx, y + dy) def _gc_correction(self, radius, theta, r_corr=Quantity(2.857, "kpc"), random_state="random-seed"): """Correction of source distribution towards the galactic center. To avoid spiralarm features near the Galactic Center, the position angle theta is blurred by a certain amount towards the GC. Parameters ---------- radius : `~astropy.units.Quantity` Radius coordinate theta : `~astropy.units.Quantity` Angle coordinate r_corr : `~astropy.units.Quantity`, optional Scale of the correction towards the GC random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`} Defines random number generator initialisation. Passed to `~gammapy.utils.random.get_random_state`. """ random_state = get_random_state(random_state) theta_corr = Quantity(random_state.uniform(0, 2 * np.pi, radius.size), "rad") return radius, theta + theta_corr * np.exp(-radius / r_corr) def __call__(self, radius, blur=True, random_state="random-seed"): """Draw random position from spiral arm distribution. Returns the corresponding angle theta[rad] to a given radius[kpc] and number of spiralarm. Possible numbers are: * Norma = 0, * Carina Sagittarius = 1, * Perseus = 2 * Crux Scutum = 3. Parameters ---------- random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`} Defines random number generator initialisation. Passed to `~gammapy.utils.random.get_random_state`. Returns ------- Returns dx and dy, if blurring= true. """ random_state = get_random_state(random_state) # Choose spiral arm N = random_state.randint(0, 4, radius.size) theta = self.k[N] * np.log(radius / self.r_0[N]) + self.theta_0[N] spiralarm = self.spiralarms[N] if blur: # Apply blurring model according to Faucher radius, theta = self._blur(radius, theta, random_state=random_state) radius, theta = self._gc_correction(radius, theta, random_state=random_state) return radius, theta, spiralarm
def make_test_eventlist( observation_table, obs_id, sigma=Angle(5.0, "deg"), spectral_index=2.7, random_state="random-seed" ): """ Make a test event list for a specified observation. The observation can be specified with an observation table object and the observation ID pointing to the correct observation in the table. For now, only a very rudimentary event list is generated, containing only detector X, Y coordinates (a.k.a. nominal system) and energy columns for the events. And the livetime of the observations stored in the header. The model used to simulate events is also very simple. Only dummy background is created (no signal). The background is created following a 2D symmetric gaussian model for the spatial coordinates (X, Y) and a power-law in energy. The gaussian width varies in energy from sigma/2 to sigma. The number of events generated depends linearly on the livetime and on the altitude angle of the observation. The model can be tuned via the sigma and spectral_index parameters. In addition, an effective area table is produced. For the moment only the low energy threshold is filled. See also :ref:`datasets_obssim`. Parameters ---------- observation_table : `~gammapy.data.ObservationTable` Observation table containing the observation to fake. obs_id : int Observation ID of the observation to fake inside the observation table. sigma : `~astropy.coordinates.Angle`, optional Width of the gaussian model used for the spatial coordinates. spectral_index : double, optional Index for the power-law model used for the energy coordinate. random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}, optional Defines random number generator initialisation. Passed to `~gammapy.utils.random.get_random_state`. Returns ------- event_list : `~gammapy.data.EventList` Event list. aeff_hdu : `~astropy.io.fits.BinTableHDU` Effective area table. """ from ..data import EventList random_state = get_random_state(random_state) # find obs row in obs table obs_ids = observation_table["OBS_ID"].data obs_index = np.where(obs_ids == obs_id) row = obs_index[0][0] # get observation information alt = Angle(observation_table["ALT"])[row] livetime = Quantity(observation_table["LIVETIME"])[row] # number of events to simulate # it is linearly dependent on the livetime, taking as reference # a trigger rate of 300 Hz # it is linearly dependent on the zenith angle (90 deg - altitude) # it is n_events_max at alt = 90 deg and n_events_max/2 at alt = 0 deg n_events_max = Quantity(300.0, "Hz") * livetime alt_min = Angle(0.0, "deg") alt_max = Angle(90.0, "deg") slope = (n_events_max - n_events_max / 2) / (alt_max - alt_min) free_term = n_events_max / 2 - slope * alt_min n_events = alt * slope + free_term # simulate energy # the index of `~numpy.random.RandomState.power` has to be # positive defined, so it is necessary to translate the (0, 1) # interval of the random variable to (emax, e_min) in order to # have a decreasing power-law e_min = Quantity(0.1, "TeV") e_max = Quantity(100.0, "TeV") energy = sample_powerlaw(e_min.value, e_max.value, spectral_index, size=n_events, random_state=random_state) energy = Quantity(energy, "TeV") E_0 = Quantity(1.0, "TeV") # reference energy for the model # define E dependent sigma # it is defined via a PL, in order to be log-linear # it is equal to the parameter sigma at E max # and sigma/2. at E min sigma_min = sigma / 2.0 # at E min sigma_max = sigma # at E max s_index = np.log(sigma_max / sigma_min) s_index /= np.log(e_max / e_min) s_norm = sigma_min * ((e_min / E_0) ** -s_index) sigma = s_norm * ((energy / E_0) ** s_index) # simulate detx, dety detx = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events), "deg") dety = Angle(random_state.normal(loc=0, scale=sigma.deg, size=n_events), "deg") # fill events in an event list event_list = EventList() event_list["DETX"] = detx event_list["DETY"] = dety event_list["ENERGY"] = energy # store important info in header event_list.meta["LIVETIME"] = livetime.to("second").value event_list.meta["EUNIT"] = str(energy.unit) # effective area table aeff_table = Table() # fill threshold, for now, a default 100 GeV will be set # independently of observation parameters energy_threshold = Quantity(0.1, "TeV") aeff_table.meta["LO_THRES"] = energy_threshold.value aeff_table.meta["name"] = "EFFECTIVE AREA" # convert to BinTableHDU and add necessary comment for the units aeff_hdu = table_to_fits_table(aeff_table) aeff_hdu.header.comments["LO_THRES"] = "[" + str(energy_threshold.unit) + "]" return event_list, aeff_hdu
def image_profile(profile_axis, image, lats, lons, binsz, counts=None, mask=None, errors=False, standard_error=0.1): """Creates a latitude or longitude profile from input flux image HDU. Parameters ---------- profile_axis : String, {'lat', 'lon'} Specified whether galactic latitude ('lat') or longitude ('lon') profile is to be returned. image : `~astropy.io.fits.ImageHDU` Image HDU object to produce GLAT or GLON profile. lats : array_like Specified as [GLAT_min, GLAT_max], with GLAT_min and GLAT_max as floats. A 1x2 array specifying the maximum and minimum latitudes to include in the region of the image for which the profile is formed, which should be within the spatial bounds of the image. lons : array_like Specified as [GLON_min, GLON_max], with GLON_min and GLON_max as floats. A 1x2 array specifying the maximum and minimum longitudes to include in the region of the image for which the profile is formed, which should be within the spatial bounds of the image. binsz : float Latitude bin size of the resulting latitude profile. This should be no less than 5 times the pixel resolution. counts : `~astropy.io.fits.ImageHDU` Counts image to allow Poisson errors to be calculated. If not provided, a standard_error should be provided, or zero errors will be returned. (Optional). mask : array_like 2D mask array, matching spatial dimensions of input image. (Optional). A mask value of True indicates a value that should be ignored, while a mask value of False indicates a valid value. errors : bool If True, computes errors, if possible, according to provided inputs. If False (default), returns all errors as zero. standard_error : float If counts image is not provided, but error values required, this specifies a standard fractional error to be applied to values. Default = 0.1. Returns ------- table : `~astropy.table.Table` Galactic latitude or longitude profile as table, with latitude bin boundaries, profile values and errors. """ lon, lat = coordinates(image) mask_init = (lats[0] <= lat) & (lat < lats[1]) mask_bounds = mask_init & (lons[0] <= lon) & (lon < lons[1]) if mask != None: mask = mask_bounds & mask else: mask = mask_bounds # Need to preserve shape here so use multiply cut_image = image.data * mask if counts != None: cut_counts = counts.data * mask values = [] count_vals = [] if profile_axis == 'lat': bins = np.arange((lats[1] - lats[0]) / binsz) glats_min = lats[0] + bins[:-1] * binsz glats_max = lats[0] + bins[1:] * binsz # Table is required here to avoid rounding problems with floats bounds = Table([glats_min, glats_max], names=('GLAT_MIN', 'GLAT_MAX')) for bin in bins[:-1]: lat_mask = (bounds['GLAT_MIN'][bin] <= lat) & (lat < bounds['GLAT_MAX'][bin]) lat_band = cut_image[lat_mask] values.append(lat_band.sum()) if counts != None: count_band = cut_counts[lat_mask] count_vals.append(count_band.sum()) else: count_vals.append(0) elif profile_axis == 'lon': bins = np.arange((lons[1] - lons[0]) / binsz) glons_min = lons[0] + bins[:-1] * binsz glons_max = lons[0] + bins[1:] * binsz # Table is required here to avoid rounding problems with floats bounds = Table([glons_min, glons_max], names=('GLON_MIN', 'GLON_MAX')) for bin in bins[:-1]: lon_mask = (bounds['GLON_MIN'][bin] <= lon) & (lon < bounds['GLON_MAX'][bin]) lon_band = cut_image[lon_mask] values.append(lon_band.sum()) if counts != None: count_band = cut_counts[lon_mask] count_vals.append(count_band.sum()) else: count_vals.append(0) if errors == True: if counts != None: rel_errors = 1. / np.sqrt(count_vals) error_vals = values * rel_errors else: error_vals = values * (np.ones(len(values)) * standard_error) else: error_vals = np.zeros_like(values) if profile_axis == 'lat': table = Table([ Quantity(glats_min, 'deg'), Quantity(glats_max, 'deg'), values, error_vals ], names=('GLAT_MIN', 'GLAT_MAX', 'BIN_VALUE', 'BIN_ERR')) elif profile_axis == 'lon': table = Table([ Quantity(glons_min, 'deg'), Quantity(glons_max, 'deg'), values, error_vals ], names=('GLON_MIN', 'GLON_MAX', 'BIN_VALUE', 'BIN_ERR')) return table