def test_mixed_axes(): label_axis = LabelMapAxis(labels=["label-1", "label-2", "label-3"], name="label") time_axis = TimeMapAxis( edges_min=[1, 10] * u.day, edges_max=[2, 13] * u.day, reference_time=Time("2020-03-19"), ) energy_axis = MapAxis.from_energy_bounds("1 TeV", "10 TeV", nbin=4) axes = MapAxes(axes=[energy_axis, time_axis, label_axis]) coords = axes.get_coord() assert coords["label"].shape == (1, 1, 3) assert coords["energy"].shape == (4, 1, 1) assert coords["time"].shape == (1, 2, 1) idx = axes.coord_to_idx(coords) assert_allclose(idx[0], np.arange(4).reshape((4, 1, 1))) assert_allclose(idx[1], np.arange(2).reshape((1, 2, 1))) assert_allclose(idx[2], np.arange(3).reshape((1, 1, 3))) hdu = axes.to_table_hdu(format="gadf") table = Table.read(hdu) assert table["LABEL"].dtype == np.dtype("<U7") assert len(table) == 24
def __init__( self, energy_axis_true, offset_axis, rad_axis, psf_value, energy_thresh_lo=u.Quantity(0.1, "TeV"), energy_thresh_hi=u.Quantity(100, "TeV"), interp_kwargs=None, ): axes = MapAxes([energy_axis_true, offset_axis, rad_axis]) axes.assert_names(["energy_true", "offset", "rad"]) if psf_value.shape != axes.shape: raise ValueError("PSF has wrong shape" f", expected {axes.shape}, got {psf_value.shape}") self._energy_axis_true = energy_axis_true self._offset_axis = offset_axis self._rad_axis = rad_axis self.psf_value = psf_value.to("sr^-1") self.energy_thresh_lo = energy_thresh_lo.to("TeV") self.energy_thresh_hi = energy_thresh_hi.to("TeV") self._interp_kwargs = interp_kwargs or {}
def to_psf3d(self, rad=None): """Create a PSF3D from a parametric PSF. It will be defined on the same energy and offset values than the input psf. Parameters ---------- rad : `~astropy.units.Quantity` Rad values Returns ------- psf3d : `~gammapy.irf.PSF3D` PSF3D. """ from gammapy.irf import PSF3D from gammapy.datasets.map import RAD_AXIS_DEFAULT offset_axis = self.axes["offset"] energy_axis_true = self.axes["energy_true"] if rad is None: rad_axis = RAD_AXIS_DEFAULT.center else: rad_axis = MapAxis.from_edges(rad, name="rad") axes = MapAxes([energy_axis_true, offset_axis, rad_axis]) data = self.evaluate(**axes.get_coord()) return PSF3D(axes=axes, data=data.value, unit=data.unit, meta=self.meta.copy())
def __init__( self, energy_axis_true, rad_axis, exposure=None, psf_value=None, interp_kwargs=None, ): self._rad_axis = rad_axis self._energy_axis_true = energy_axis_true axes = MapAxes([energy_axis_true, rad_axis]) axes.assert_names(["energy_true", "rad"]) if exposure is None: self.exposure = u.Quantity(np.ones(self.energy_axis_true.nbin), "cm^2 s") else: self.exposure = u.Quantity(exposure).to("cm^2 s") if psf_value is None: self.psf_value = np.zeros(axes.shape) * u.Unit("sr^-1") else: if np.shape(psf_value) != axes.shape: raise ValueError( "psf_value has wrong shape" f", expected {axes.shape}, got {np.shape(psf_value)}" ) self.psf_value = u.Quantity(psf_value).to("sr^-1") self._interp_kwargs = interp_kwargs or {}
def to_hdulist(self): """ Convert PSF table data to FITS HDU list. Returns ------- hdu_list : `~astropy.io.fits.HDUList` PSF in HDU list format. """ axes = MapAxes([self.energy_axis_true, self.offset_axis]) table = axes.to_table(format="gadf-dl3") # Set up data names = ["SIGMA", "GAMMA"] units = ["deg", ""] data = [ self.sigma, self.gamma, ] for name_, data_, unit_ in zip(names, data, units): table[name_] = [data_] table[name_].unit = unit_ hdu = fits.BinTableHDU(table) hdu.header.update(self.meta) return fits.HDUList([fits.PrimaryHDU(), hdu])
def __init__( self, axes, data=0, unit="", is_pointlike=False, fov_alignment=FoVAlignment.RADEC, meta=None, interp_kwargs=None, ): axes = MapAxes(axes) axes.assert_names(self.required_axes) self._axes = axes self._fov_alignment = FoVAlignment(fov_alignment) self._is_pointlike = is_pointlike if isinstance(data, u.Quantity): self.data = data.value if not self.default_unit.is_equivalent(data.unit): raise ValueError( f"Error: {data.unit} is not an allowed unit. {self.tag} requires {self.default_unit} data quantities." ) else: self._unit = data.unit else: self.data = data self._unit = unit self.meta = meta or {} if interp_kwargs is None: interp_kwargs = self.default_interp_kwargs.copy() self.interp_kwargs = interp_kwargs
def __init__(self, axes, data=0, unit="", meta=None): axes = MapAxes(axes) axes.assert_names(self.required_axes) self._axes = axes self.data = data self.unit = unit self.meta = meta or {}
def to_energy_dependent_table_psf(self, offset, rad=None): """Convert to energy-dependent table PSF. Parameters ---------- offset : `~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. Returns ------- table_psf : `~gammapy.irf.EnergyDependentTablePSF` Energy-dependent PSF """ from gammapy.irf import EnergyDependentTablePSF from gammapy.datasets.map import RAD_AXIS_DEFAULT energy_axis_true = self.axes["energy_true"] if rad is None: rad_axis = RAD_AXIS_DEFAULT else: rad_axis = MapAxis.from_edges(rad, name="rad") axes = MapAxes([energy_axis_true, rad_axis]) data = self.evaluate(**axes.get_coord(), offset=offset) return EnergyDependentTablePSF(axes=axes, data=data.value, unit=data.unit)
def from_parametrization(cls, energy_axis_true=None, instrument="HESS"): r"""Create parametrized effective area. Parametrizations of the effective areas of different Cherenkov telescopes taken from Appendix B of Abramowski et al. (2010), see https://ui.adsabs.harvard.edu/abs/2010MNRAS.402.1342A . .. math:: A_{eff}(E) = g_1 \left(\frac{E}{\mathrm{MeV}}\right)^{-g_2}\exp{\left(-\frac{g_3}{E}\right)} This method does not model the offset dependence of the effective area, but just assumes that it is constant. Parameters ---------- energy_axis_true : `MapAxis` Energy binning, analytic function is evaluated at log centers instrument : {'HESS', 'HESS2', 'CTA'} Instrument name Returns ------- aeff : `EffectiveAreaTable2D` Effective area table """ # Put the parameters g in a dictionary. # Units: g1 (cm^2), g2 (), g3 (MeV) pars = { "HESS": [6.85e9, 0.0891, 5e5], "HESS2": [2.05e9, 0.0891, 1e5], "CTA": [1.71e11, 0.0891, 1e5], } if instrument not in pars.keys(): ss = f"Unknown instrument: {instrument}\n" ss += f"Valid instruments: {list(pars.keys())}" raise ValueError(ss) if energy_axis_true is None: energy_axis_true = MapAxis.from_energy_bounds("2 GeV", "200 TeV", nbin=20, per_decade=True, name="energy_true") g1, g2, g3 = pars[instrument] offset_axis = MapAxis.from_edges([0., 5.] * u.deg, name="offset") axes = MapAxes([energy_axis_true, offset_axis]) coords = axes.get_coord() energy, offset = coords["energy_true"].to_value( "MeV"), coords["offset"] data = np.ones_like(offset.value) * g1 * energy**(-g2) * np.exp( -g3 / energy) # TODO: fake offset dependence? meta = {"TELESCOP": instrument} return cls(axes=axes, data=data, unit="cm2", meta=meta)
def to_table(self): """Convert to `~astropy.table.Table`.""" # TODO: fix axis order axes = MapAxes(self.data.axes[::-1]) table = axes.to_table(format="gadf-dl3") table.meta = self.meta.copy() table["BKG"] = self.data.data.T[np.newaxis] return table
def test_map_axes_pad(): axis_1 = MapAxis.from_energy_bounds("1 TeV", "10 TeV", nbin=1) axis_2 = MapAxis.from_bounds(0, 1, nbin=2, unit="deg", name="rad") axes = MapAxes([axis_1, axis_2]) axes = axes.pad(axis_name="energy", pad_width=1) assert_allclose(axes["energy"].edges, [0.1, 1, 10, 100] * u.TeV)
def __init__(self, axes, data=0, unit="", meta=None, interp_kwargs=None): axes = MapAxes(axes) axes.assert_names(self.required_axes) self._axes = axes self.data = data self.unit = unit self.meta = meta or {} if interp_kwargs is None: interp_kwargs = self.default_interp_kwargs.copy() self.interp_kwargs = interp_kwargs
def to_edisp_kernel(self, offset, energy_true=None, energy=None): """Detector response R(Delta E_reco, Delta E_true) Probability to reconstruct an energy in a given true energy band in a given reconstructed energy band Parameters ---------- offset : `~astropy.coordinates.Angle` Offset energy_true : `~astropy.units.Quantity`, None True energy axis energy : `~astropy.units.Quantity` Reconstructed energy axis Returns ------- edisp : `~gammapy.irf.EDispKernel` Energy dispersion matrix """ offset = Angle(offset) # TODO: expect directly MapAxis here? if energy is None: energy_axis = self.axes["energy_true"].copy(name="energy") else: energy_axis = MapAxis.from_energy_edges(energy) if energy_true is None: energy_axis_true = self.axes["energy_true"] else: energy_axis_true = MapAxis.from_energy_edges( energy_true, name="energy_true", ) axes = MapAxes([energy_axis_true, energy_axis]) coords = axes.get_coord(mode="edges", axis_name="energy") # migration value of energy bounds migra = coords["energy"] / coords["energy_true"] values = self.integral( axis_name="migra", offset=offset, energy_true=coords["energy_true"], migra=migra, ) data = np.diff(values) return EDispKernel( axes=axes, data=data.to_value(""), )
def from_gauss(cls, energy_axis_true, migra_axis, offset_axis, bias, sigma, pdf_threshold=1e-6): """Create Gaussian energy dispersion matrix (`EnergyDispersion2D`). The output matrix will be Gaussian in (energy_true / energy). The ``bias`` and ``sigma`` should be either floats or arrays of same dimension than ``energy_true``. ``bias`` refers to the mean value of the ``migra`` distribution minus one, i.e. ``bias=0`` means no bias. Note that, the output matrix is flat in offset. Parameters ---------- energy_axis_true : `MapAxis` True energy axis migra_axis : `~astropy.units.Quantity` Migra axis offset_axis : `~astropy.units.Quantity` Bin edges of offset bias : float or `~numpy.ndarray` Center of Gaussian energy dispersion, bias sigma : float or `~numpy.ndarray` RMS width of Gaussian energy dispersion, resolution. pdf_threshold : float, optional Zero suppression threshold """ axes = MapAxes([energy_axis_true, migra_axis, offset_axis]) coords = axes.get_coord(mode="edges", axis_name="migra") migra_min = coords["migra"][:, :-1, :] migra_max = coords["migra"][:, 1:, :] # Analytical formula for integral of Gaussian s = np.sqrt(2) * sigma t1 = (migra_max - 1 - bias) / s t2 = (migra_min - 1 - bias) / s pdf = (scipy.special.erf(t1) - scipy.special.erf(t2)) / 2 pdf = pdf / (migra_max - migra_min) # no offset dependence data = pdf * np.ones(axes.shape) data[data < pdf_threshold] = 0 return cls( axes=axes, data=data.value, )
def __init__(self, axes, data=0, unit="", meta=None, interp_kwargs=None): axes = MapAxes(axes) axes.assert_names(self.required_axes) self._axes = axes if isinstance(data, u.Quantity): self.data = data.value self.unit = data.unit else: self.data = data self.unit = unit self.meta = meta or {} if interp_kwargs is None: interp_kwargs = self.default_interp_kwargs.copy() self.interp_kwargs = interp_kwargs
def test_write(self): energy_axis_true = MapAxis.from_energy_bounds("1 TeV", "10 TeV", nbin=10, name="energy_true") offset_axis = MapAxis.from_bounds(0, 1, nbin=3, unit="deg", name="offset", node_type="edges") migra_axis = MapAxis.from_bounds(0, 3, nbin=3, name="migra", node_type="edges") axes = MapAxes([energy_axis_true, migra_axis, offset_axis]) data = np.ones(shape=axes.shape) edisp = EnergyDispersion2D(axes=axes, data=data) hdu = edisp.to_table_hdu() energy = edisp.axes["energy_true"].edges assert_equal(hdu.data["ENERG_LO"][0], energy[:-1].value) assert hdu.header["TUNIT1"] == edisp.axes["energy_true"].unit
def from_table(cls, table): """Create from `~astropy.table.Table`. Parameters ---------- table : `~astropy.table.Table` Table with effective area. Returns ------- aeff : `EffectiveArea2D` Effective area """ if "ENERG_LO" in table.colnames: column_prefixes = ["ENERG", "THETA", "MIGRA"] elif "ETRUE_LO" in table.colnames: column_prefixes = ["ETRUE", "THETA", "MIGRA"] else: raise ValueError( 'Invalid column names. Need "ENERG_LO/ENERG_HI" or "ETRUE_LO/ETRUE_HI"' ) axes = MapAxes.from_table(table, column_prefixes=column_prefixes, format="gadf-dl3") data = table["MATRIX"].quantity[0].transpose() return cls( energy_axis_true=axes["energy_true"], offset_axis=axes["offset"], migra_axis=axes["migra"], data=data, )
def from_table(cls, table, format="gadf-dl3"): """Read from `~astropy.table.Table`. Parameters ---------- table : `~astropy.table.Table` Table with irf data format : {"gadf-dl3"} Format specification Returns ------- irf : `IRF` IRF class. """ axes = MapAxes.from_table(table=table, format=format)[cls.required_axes] column_name = IRF_DL3_HDU_SPECIFICATION[cls.tag]["column_name"] data = table[column_name].quantity[0].transpose() return cls( axes=axes, data=data.value, meta=table.meta, unit=data.unit, is_pointlike=gadf_is_pointlike(table.meta), fov_alignment=table.meta.get("FOVALIGN", "RADEC"), )
def test_write(self): energy_axis_true = MapAxis.from_energy_bounds( "1 TeV", "10 TeV", nbin=10, name="energy_true" ) offset_axis = MapAxis.from_bounds( 0, 1, nbin=3, unit="deg", name="offset", node_type="edges" ) migra_axis = MapAxis.from_bounds(0, 3, nbin=3, name="migra", node_type="edges") axes = MapAxes([energy_axis_true, migra_axis, offset_axis]) data = np.ones(shape=axes.shape) edisp_test = EnergyDispersion2D(axes=axes) with pytest.raises(ValueError) as error: wrong_unit = u.m**2 EnergyDispersion2D(axes=axes, data=data * wrong_unit) assert error.match(f"Error: {wrong_unit} is not an allowed unit. {edisp_test.tag} requires {edisp_test.default_unit} data quantities.") edisp = EnergyDispersion2D(axes=axes, data=data) hdu = edisp.to_table_hdu() energy = edisp.axes["energy_true"].edges assert_equal(hdu.data["ENERG_LO"][0], energy[:-1].value) assert hdu.header["TUNIT1"] == edisp.axes["energy_true"].unit
def from_table(cls, table): """Create from `~astropy.table.Table` in ARF format. Data format specification: :ref:`gadf:ogip-arf` """ axes = MapAxes.from_table(table, format="ogip-arf")[cls.required_axes] data = table["SPECRESP"].quantity return cls(axes=axes, data=data.value, unit=data.unit)
def from_table(cls, table, format="gadf-dl3"): """Read from `~astropy.table.Table`. Parameters ---------- table : `~astropy.table.Table` Table with background data format : {"gadf-dl3"} Format specification Returns ------- bkg : `Background2D` or `Background2D` Background IRF class. """ # TODO: some of the existing background files have missing HDUCLAS keywords # which are required to define the correct Gammapy axis names if "HDUCLAS2" not in table.meta: log.warning("Missing 'HDUCLAS2' keyword assuming 'BKG'") table = table.copy() table.meta["HDUCLAS2"] = "BKG" axes = MapAxes.from_table(table, format=format)[cls.required_axes] # TODO: spec says key should be "BKG", but there are files around # (e.g. CTA 1DC) that use "BGD". For now we support both if "BKG" in table.colnames: bkg_name = "BKG" elif "BGD" in table.colnames: bkg_name = "BGD" else: raise ValueError("Invalid column names. Need 'BKG' or 'BGD'.") data = table[bkg_name].quantity[0].T if data.unit == "" or isinstance(data.unit, u.UnrecognizedUnit): data = u.Quantity(data.value, "s-1 MeV-1 sr-1", copy=False) log.warning( "Invalid unit found in background table! Assuming (s-1 MeV-1 sr-1)" ) # TODO: The present HESS and CTA background fits files # have a reverse order (lon, lat, E) than recommended in GADF(E, lat, lon) # For now, we support both. if axes.shape == axes.shape[::-1]: log.error("Ambiguous axes order in Background fits files!") if np.shape(data) != axes.shape: log.debug("Transposing background table on read") data = data.transpose() return cls( axes=axes, data=data.value, meta=table.meta, unit=data.unit, is_pointlike=gadf_is_pointlike(table.meta), fov_alignment=table.meta.get("FOVALIGN", "RADEC"), )
def get_edisp_kernel(self, position=None, energy_axis=None): """Get energy dispersion at a given position. Parameters ---------- position : `~astropy.coordinates.SkyCoord` the target position. Should be a single coordinates energy_axis : `MapAxis` Reconstructed energy axis Returns ------- edisp : `~gammapy.irf.EnergyDispersion` the energy dispersion (i.e. rmf object) """ if position is None: position = self.edisp_map.geom.center_skydir if position.size != 1: raise ValueError( "EnergyDispersion can be extracted at one single position only." ) position = self._get_nearest_valid_position(position) energy_axis_true = self.edisp_map.geom.axes["energy_true"] axes = MapAxes([energy_axis_true, energy_axis]) coords = axes.get_coord(mode="edges", axis_name="energy") # migration value of energy bounds migra = coords["energy"] / coords["energy_true"] coords = { "skycoord": position, "energy_true": coords["energy_true"], "migra": migra, } values = self.edisp_map.integral(axis_name="migra", coords=coords) data = np.diff(np.clip(values, 0, np.inf)) return EDispKernel(axes=axes, data=data.to_value(""))
def to_3d(self): """ "Convert to Background3D""" edges = np.concatenate(( np.negative(self.axes["offset"].edges)[::-1][:-1], self.axes["offset"].edges, )) fov_lat = MapAxis.from_edges(edges=edges, name="fov_lat") fov_lon = MapAxis.from_edges(edges=edges, name="fov_lon") axes = MapAxes([self.axes["energy"], fov_lon, fov_lat]) coords = axes.get_coord() offset = np.sqrt(coords["fov_lat"]**2 + coords["fov_lon"]**2) data = self.evaluate(offset=offset, energy=coords["energy"]) return Background3D( axes=axes, data=data, )
def to_hdulist(self): """Convert PSF table data to FITS HDU list. Returns ------- hdu_list : `~astropy.io.fits.HDUList` PSF in HDU list format. """ axes = MapAxes( [self.offset_axis, self.energy_axis_true, self.rad_axis]) table = axes.to_table(format="gadf-dl3") table["RPSF"] = self.psf_value.T[np.newaxis] 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 to_hdulist(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 = [ "SCALE", "SIGMA_1", "AMPL_2", "SIGMA_2", "AMPL_3", "SIGMA_3", ] units = ["", "deg", "", "deg", "", "deg"] data = [ self.norms[0], self.sigmas[0], self.norms[1], self.sigmas[1], self.norms[2], self.sigmas[2], ] axes = MapAxes([self.energy_axis_true, self.offset_axis]) table = axes.to_table(format="gadf-dl3") 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 __init__( self, energy_axis_true, offset_axis, rad_axis, data, meta=None, interp_kwargs=None, ): interp_kwargs = interp_kwargs or {} axes = MapAxes([energy_axis_true, offset_axis, rad_axis]) axes.assert_names(["energy_true", "offset", "rad"]) self.data = NDDataArray(axes=axes, data=u.Quantity(data).to("sr^-1"), interp_kwargs=interp_kwargs) self.meta = meta or {}
def from_gauss(cls, energy_axis_true, rad_axis=None, sigma=0.1 * u.deg): """Create all -sky PSF map from Gaussian width. This is used for testing and examples. The width can be the same for all energies or be an array with one value per energy node. It does not depend on position. Parameters ---------- energy_axis_true : `~gammapy.maps.MapAxis` True energy axis. rad_axis : `~gammapy.maps.MapAxis` Offset angle wrt source position axis. sigma : `~astropy.coordinates.Angle` Gaussian width. Returns ------- psf_map : `PSFMap` Point spread function map. """ from gammapy.datasets.map import RAD_AXIS_DEFAULT if rad_axis is None: rad_axis = RAD_AXIS_DEFAULT.copy() axes = MapAxes([energy_axis_true, rad_axis]) coords = axes.get_coord() sigma = np.broadcast_to(u.Quantity(sigma), energy_axis_true.nbin, subok=True) gauss = Gauss2DPDF(sigma=sigma.reshape((-1, 1))) data = gauss(coords["rad"]) table_psf = EnergyDependentTablePSF(axes=axes, unit=data.unit, data=data.value) return cls.from_energy_dependent_table_psf(table_psf)
def from_table(cls, table): """Read from `~astropy.table.Table`.""" axes = MapAxes.from_table(table=table, column_prefixes=["ENERG", "THETA"], format="gadf-dl3") return cls( energy_axis_true=axes["energy_true"], offset_axis=axes["offset"], data=table["EFFAREA"].quantity[0].transpose(), meta=table.meta, )
def __init__( self, energy_axis_true, rad_axis, exposure=None, data=None, interp_kwargs=None, ): interp_kwargs = interp_kwargs or {} axes = MapAxes([energy_axis_true, rad_axis]) axes.assert_names(["energy_true", "rad"]) self.data = NDDataArray(axes=axes, data=u.Quantity(data).to("sr^-1"), interp_kwargs=interp_kwargs) if exposure is None: self.exposure = u.Quantity(np.ones(self.energy_axis_true.nbin), "cm^2 s") else: self.exposure = u.Quantity(exposure).to("cm^2 s")
def from_table(cls, table, format="gadf-dl3"): """Read from `~astropy.table.Table`. Parameters ---------- table : `~astropy.table.Table` Table with background data format : {"gadf-dl3"} Format specification Returns ------- bkg : `Background2D` or `Background2D` Background IRF class. """ axes = MapAxes.from_table(table, format=format)[cls.required_axes] # Spec says key should be "BKG", but there are files around # (e.g. CTA 1DC) that use "BGD". For now we support both if "BKG" in table.colnames: bkg_name = "BKG" elif "BGD" in table.colnames: bkg_name = "BGD" else: raise ValueError('Invalid column names. Need "BKG" or "BGD".') data = table[bkg_name].quantity[0].T if data.unit == "" or isinstance(data.unit, u.UnrecognizedUnit): data = u.Quantity(data.value, "s-1 MeV-1 sr-1", copy=False) log.warning( "Invalid unit found in background table! Assuming (s-1 MeV-1 sr-1)" ) # TODO: The present HESS and CTA backgroundfits files # have a reverse order (lon, lat, E) than recommened in GADF(E, lat, lon) # For now, we suport both. if axes.shape == axes.shape[::-1]: log.error("Ambiguous axes order in Background fits files!") if np.shape(data) != axes.shape: log.debug("Transposing background table on read") data = data.transpose() return cls( axes=axes, data=data.value, meta=table.meta, unit=data.unit )