def test_raise_to_power(self, power): """Check that raising LogQuantities to some power is only possible when the physical unit is dimensionless, and that conversion is turned off when the resulting logarithmic unit (say, mag**2) is incompatible.""" lq = u.Magnitude(np.arange(1., 4.)*u.Jy) if power == 0: assert np.all(lq ** power == 1.) elif power == 1: assert np.all(lq ** power == lq) else: with pytest.raises(u.UnitsError): lq ** power # with dimensionless, it works, but falls back to normal quantity # (except for power=1) lq2 = u.Magnitude(np.arange(10.)) t = lq2**power if power == 0: assert t.unit is u.dimensionless_unscaled assert np.all(t.value == 1.) elif power == 1: assert np.all(t == lq2) else: assert not isinstance(t, type(lq2)) assert t.unit == lq2.unit.function_unit ** power with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(u.dimensionless_unscaled)
def test_raise_to_power(self, power): """Check that raising LogUnits to some power is only possible when the physical unit is dimensionless, and that conversion is turned off when the resulting logarithmic unit (such as mag**2) is incompatible.""" lu1 = u.mag(u.Jy) if power == 0: assert lu1**power == u.dimensionless_unscaled elif power == 1: assert lu1**power == lu1 else: with pytest.raises(u.UnitsError): lu1**power # With dimensionless, though, it works, but returns a normal unit. lu2 = u.mag(u.dimensionless_unscaled) t = lu2**power if power == 0: assert t == u.dimensionless_unscaled elif power == 1: assert t == lu2 else: assert not isinstance(t, type(lu2)) assert t == lu2.function_unit**power # also check we roundtrip t2 = t**(1. / power) assert t2 == lu2.function_unit with u.set_enabled_equivalencies(u.logarithmic()): assert_allclose(t2.to(u.dimensionless_unscaled, np.arange(3.)), lu2.to(lu2.physical_unit, np.arange(3.)))
def test_raise_to_power(self, power): """Check that raising LogUnits to some power is only possible when the physical unit is dimensionless, and that conversion is turned off when the resulting logarithmic unit (such as mag**2) is incompatible.""" lu1 = u.mag(u.Jy) if power == 0: assert lu1 ** power == u.dimensionless_unscaled elif power == 1: assert lu1 ** power == lu1 else: with pytest.raises(u.UnitsError): lu1 ** power # With dimensionless, though, it works, but returns a normal unit. lu2 = u.mag(u.dimensionless_unscaled) t = lu2**power if power == 0: assert t == u.dimensionless_unscaled elif power == 1: assert t == lu2 else: assert not isinstance(t, type(lu2)) assert t == lu2.function_unit**power # also check we roundtrip t2 = t**(1./power) assert t2 == lu2.function_unit with u.set_enabled_equivalencies(u.logarithmic()): assert_allclose(t2.to(u.dimensionless_unscaled, np.arange(3.)), lu2.to(lu2.physical_unit, np.arange(3.)))
def test_raise_to_power(self, power): """Check that raising LogQuantities to some power is only possible when the physical unit is dimensionless, and that conversion is turned off when the resulting logarithmic unit (say, mag**2) is incompatible.""" lq = u.Magnitude(np.arange(1., 4.) * u.Jy) if power == 0: assert np.all(lq**power == 1.) elif power == 1: assert np.all(lq**power == lq) else: with pytest.raises(u.UnitsError): lq**power # with dimensionless, it works, but falls back to normal quantity # (except for power=1) lq2 = u.Magnitude(np.arange(10.)) t = lq2**power if power == 0: assert t.unit is u.dimensionless_unscaled assert np.all(t.value == 1.) elif power == 1: assert np.all(t == lq2) else: assert not isinstance(t, type(lq2)) assert t.unit == lq2.unit.function_unit**power with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(u.dimensionless_unscaled)
def to_color(self, wfb): r"""Express as a color index. Parameters ---------- wfb : two-element `~astropy.units.Quantity` or tuple Wavelengths, frequencies, or bandpasses of the measurement. If a bandpass, the effective wavelength of a solar spectrum will be used. Bandpasses may be a string (name) or `~synphot.SpectralElement` (see :func:`~sbpy.spectroscopy.sun.Sun.filt`). Notes ----- Color index is computed from: .. math:: α = \frac{1 + S Δλ / 2}{1 - S * Δλ / 2} where S is the spectral gradient at the mean of λ0 and λ1, and: .. math:: α = R(λ1) / R(λ0) = 10^{0.4 color_index} color_index = Δm - C_{sun} Δλ is typically expressed in units of 100 nm. Returns ------- color : `~astropy.units.Quantity` ``blue - red`` color in magnitudes, dimensionless and excludes the solar color. Examples -------- >>> import astropy.units as u >>> from sbpy.units import hundred_nm >>> S = SpectralGradient(10 * u.percent / hundred_nm, ... wave0=0.55 * u.um) >>> C = S.to_color((525, 575) * u.nm) >>> print(C) # doctest: +FLOAT_CMP 0.05429812423309064 mag """ lambda_eff = self._lambda_eff(wfb) S = self.renormalize(lambda_eff.mean()) dw = lambda_eff[0] - lambda_eff[1] beta = (S * dw / 2).decompose() # dimensionless color = ((1 + beta) / (1 - beta)).to(u.mag, u.logarithmic()) return color
def test_logarithmic(log_unit): # check conversion of mag, dB, and dex to dimensionless and vice versa with pytest.raises(u.UnitsError): log_unit.to(1, 0.) with pytest.raises(u.UnitsError): u.dimensionless_unscaled.to(log_unit) assert log_unit.to(1, 0., equivalencies=u.logarithmic()) == 1. assert u.dimensionless_unscaled.to(log_unit, equivalencies=u.logarithmic()) == 0. # also try with quantities q_dex = np.array([0., -1., 1., 2.]) * u.dex q_expected = 10.**q_dex.value * u.dimensionless_unscaled q_log_unit = q_dex.to(log_unit) assert np.all(q_log_unit.to(1, equivalencies=u.logarithmic()) == q_expected) assert np.all(q_expected.to(log_unit, equivalencies=u.logarithmic()) == q_log_unit) with u.set_enabled_equivalencies(u.logarithmic()): assert np.all(np.abs(q_log_unit - q_expected.to(log_unit)) < 1.e-10*log_unit)
def test_logarithmic(log_unit): # check conversion of mag, dB, and dex to dimensionless and vice versa with pytest.raises(u.UnitsError): log_unit.to(1, 0.) with pytest.raises(u.UnitsError): u.dimensionless_unscaled.to(log_unit) assert log_unit.to(1, 0., equivalencies=u.logarithmic()) == 1. assert u.dimensionless_unscaled.to(log_unit, equivalencies=u.logarithmic()) == 0. # also try with quantities q_dex = np.array([0., -1., 1., 2.]) * u.dex q_expected = 10.**q_dex.value * u.dimensionless_unscaled q_log_unit = q_dex.to(log_unit) assert np.all(q_log_unit.to(1, equivalencies=u.logarithmic()) == q_expected) assert np.all(q_expected.to(log_unit, equivalencies=u.logarithmic()) == q_log_unit) with u.set_enabled_equivalencies(u.logarithmic()): assert np.all(np.abs(q_log_unit - q_expected.to(log_unit)) < 1.e-10*log_unit)
def test_multiplication_division(self): """Check that multiplication/division with other quantities is only possible when the physical unit is dimensionless, and that this turns the result into a normal quantity.""" lq = u.Magnitude(np.arange(1., 11.)*u.Jy) with pytest.raises(u.UnitsError): lq * (1.*u.m) with pytest.raises(u.UnitsError): (1.*u.m) * lq with pytest.raises(u.UnitsError): lq / lq for unit in (u.m, u.mag, u.dex): with pytest.raises(u.UnitsError): lq / unit lq2 = u.Magnitude(np.arange(1, 11.)) with pytest.raises(u.UnitsError): lq2 * lq with pytest.raises(u.UnitsError): lq2 / lq with pytest.raises(u.UnitsError): lq / lq2 # but dimensionless_unscaled can be cancelled r = lq2 / u.Magnitude(2.) assert r.unit == u.dimensionless_unscaled assert np.all(r.value == lq2.value/2.) # with dimensionless, normal units OK, but return normal quantities tf = lq2 * u.m tr = u.m * lq2 for t in (tf, tr): assert not isinstance(t, type(lq2)) assert t.unit == lq2.unit.function_unit * u.m with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(lq2.unit.physical_unit) t = tf / (50.*u.cm) # now we essentially have the same quantity but with a prefactor of 2 assert t.unit.is_equivalent(lq2.unit.function_unit) assert_allclose(t.to(lq2.unit.function_unit), lq2._function_view*2)
def test_multiplication_division(self): """Check that multiplication/division with other quantities is only possible when the physical unit is dimensionless, and that this turns the result into a normal quantity.""" lq = u.Magnitude(np.arange(1., 11.) * u.Jy) with pytest.raises(u.UnitsError): lq * (1. * u.m) with pytest.raises(u.UnitsError): (1. * u.m) * lq with pytest.raises(u.UnitsError): lq / lq for unit in (u.m, u.mag, u.dex): with pytest.raises(u.UnitsError): lq / unit lq2 = u.Magnitude(np.arange(1, 11.)) with pytest.raises(u.UnitsError): lq2 * lq with pytest.raises(u.UnitsError): lq2 / lq with pytest.raises(u.UnitsError): lq / lq2 # but dimensionless_unscaled can be cancelled r = lq2 / u.Magnitude(2.) assert r.unit == u.dimensionless_unscaled assert np.all(r.value == lq2.value / 2.) # with dimensionless, normal units OK, but return normal quantities tf = lq2 * u.m tr = u.m * lq2 for t in (tf, tr): assert not isinstance(t, type(lq2)) assert t.unit == lq2.unit.function_unit * u.m with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(lq2.unit.physical_unit) t = tf / (50. * u.cm) # now we essentially have the same quantity but with a prefactor of 2 assert t.unit.is_equivalent(lq2.unit.function_unit) assert_allclose(t.to(lq2.unit.function_unit), lq2._function_view * 2)
def test_inplace_addition_subtraction(self, other): """Check that inplace addition/subtraction with quantities with magnitude or MagUnit units works, and that it changes the physical units appropriately.""" lq = u.Magnitude(np.arange(1., 10.) * u.Jy) other_physical = other.to(getattr(other.unit, 'physical_unit', u.dimensionless_unscaled), equivalencies=u.logarithmic()) lq_sf = lq.copy() lq_sf += other assert_allclose(lq_sf.physical, lq.physical * other_physical) lq_df = lq.copy() lq_df -= other assert_allclose(lq_df.physical, lq.physical / other_physical)
def test_inplace_addition_subtraction(self, other): """Check that inplace addition/subtraction with quantities with magnitude or MagUnit units works, and that it changes the physical units appropriately.""" lq = u.Magnitude(np.arange(1., 10.)*u.Jy) other_physical = other.to(getattr(other.unit, 'physical_unit', u.dimensionless_unscaled), equivalencies=u.logarithmic()) lq_sf = lq.copy() lq_sf += other assert_allclose(lq_sf.physical, lq.physical * other_physical) lq_df = lq.copy() lq_df -= other assert_allclose(lq_df.physical, lq.physical / other_physical)
def test_multiplication_division(self): """Check that multiplication/division with other units is only possible when the physical unit is dimensionless, and that this turns the unit into a normal one.""" lu1 = u.mag(u.Jy) with pytest.raises(u.UnitsError): lu1 * u.m with pytest.raises(u.UnitsError): u.m * lu1 with pytest.raises(u.UnitsError): lu1 / lu1 for unit in (u.dimensionless_unscaled, u.m, u.mag, u.dex): with pytest.raises(u.UnitsError): lu1 / unit lu2 = u.mag(u.dimensionless_unscaled) with pytest.raises(u.UnitsError): lu2 * lu1 with pytest.raises(u.UnitsError): lu2 / lu1 # But dimensionless_unscaled can be cancelled. assert lu2 / lu2 == u.dimensionless_unscaled # With dimensionless, normal units are OK, but we return a plain unit. tf = lu2 * u.m tr = u.m * lu2 for t in (tf, tr): assert not isinstance(t, type(lu2)) assert t == lu2.function_unit * u.m with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(lu2.physical_unit) # Now we essentially have a LogUnit with a prefactor of 100, # so should be equivalent again. t = tf / u.cm with u.set_enabled_equivalencies(u.logarithmic()): assert t.is_equivalent(lu2.function_unit) assert_allclose(t.to(u.dimensionless_unscaled, np.arange(3.)/100.), lu2.to(lu2.physical_unit, np.arange(3.))) # If we effectively remove lu1, a normal unit should be returned. t2 = tf / lu2 assert not isinstance(t2, type(lu2)) assert t2 == u.m t3 = tf / lu2.function_unit assert not isinstance(t3, type(lu2)) assert t3 == u.m # For completeness, also ensure non-sensical operations fail with pytest.raises(TypeError): lu1 * object() with pytest.raises(TypeError): slice(None) * lu1 with pytest.raises(TypeError): lu1 / [] with pytest.raises(TypeError): 1 / lu1
def from_color(cls, wfb, color): r"""Initialize from observed color. Parameters ---------- wfb : two-element `~astropy.units.Quantity` or tuple Wavelengths, frequencies, or bandpasses of the measurement. If a bandpass, the effective wavelength of a solar spectrum will be used. Bandpasses may be a string (name) or `~synphot.SpectralElement` (see :func:`~sbpy.spectroscopy.sun.Sun.filt`). color : `~astropy.units.Quantity`, optional Observed color, ``blue - red`` for magnitudes, ``red / blue`` for linear units. Must be dimensionless and have the solar color removed. Notes ----- Computes spectral gradient from ``color_index``. ``wfb[0]`` is the blue-ward of the two measurements .. math:: S &= \frac{R(λ1) - R(λ0)}{R(λ1) + R(λ0)} \frac{2}{Δλ} \\ &= \frac{α - 1}{α + 1} \frac{2}{Δλ} where R(λ) is the reflectivity, and: .. math:: α = R(λ1) / R(λ0) = 10^{0.4 color_index} color_index = Δm - C_{sun} Δλ is typically expressed in units of 100 nm. Examples -------- >>> import astropy.units as u >>> w = [0.4719, 0.6185] * u.um >>> S = SpectralGradient.from_color(w, 0.10 * u.mag) >>> print(S) # doctest: +FLOAT_CMP 6.27819572 % / 100 nm """ from ..units import hundred_nm lambda_eff = SpectralGradient._lambda_eff(wfb) try: # works for u.Magnitudes and dimensionless u.Quantity alpha = u.Quantity(color, u.dimensionless_unscaled) except u.UnitConversionError: # works for u.mag alpha = color.to(u.dimensionless_unscaled, u.logarithmic()) dw = lambda_eff[0] - lambda_eff[1] S = ((2 / dw * (alpha - 1) / (alpha + 1)).to(u.percent / hundred_nm)) return SpectralGradient(S, wave=lambda_eff)
def to_ref(self, eph, normalized=None, append_results=False, **kwargs): """Calculate phase function in average bidirectional reflectance Parameters ---------- eph : `~sbpy.data.Ephem`, numbers, iterables of numbers, or `~astropy.units.Quantity` If `~sbpy.data.Ephem` or dict_like, ephemerides of the object that can include phase angle, heliocentric and geocentric distances via keywords `phase`, `r` and `delta`. If float or array_like, then the phase angle of object. If any distance (heliocentric and geocentric) is not provided, then it will be assumed to be 1 au. If no unit is provided via type `~astropy.units.Quantity`, then radians is assumed for phase angle, and au is assumed for distances. normalized : number, `~astropy.units.Quantity` The angle to which the reflectance is normalized. append_results : bool Controls the return of this method. **kwargs : optional parameters accepted by `astropy.modeling.Model.__call__` Returns ------- `~astropy.units.Quantity`, array if ``append_results == False`` `~sbpy.data.Ephem` if ``append_results == True`` When ``append_results == False``: The calculated reflectance will be returned. When ``append_results == True``: If ``eph`` is a `~sbpy.data.Ephem` object, then the calculated reflectance will be appended to ``eph`` as a new column. Otherwise a new `~sbpy.data.Ephem` object is created to contain the input ``eph`` and the calculated reflectance in two columns. Examples -------- >>> import numpy as np >>> from astropy import units as u >>> from sbpy.calib import solar_fluxd >>> from sbpy.photometry import HG >>> from sbpy.data import Ephem >>> ceres_hg = HG(3.34 * u.mag, 0.12, radius = 480 * u.km, wfb= 'V') >>> # parameter `eph` as `~sbpy.data.Ephem` type >>> eph = Ephem.from_dict({'alpha': np.linspace(0,np.pi*0.9,200)*u.rad, ... 'r': np.repeat(2.7*u.au, 200), ... 'delta': np.repeat(1.8*u.au, 200)}) >>> with solar_fluxd.set({'V': -26.77 * u.mag}): ... ref1 = ceres_hg.to_ref(eph) ... # parameter `eph` as numpy array ... pha = np.linspace(0, 170, 200) * u.deg ... ref2 = ceres_hg.to_ref(pha) """ self._check_unit() pha = eph['alpha'] if len(pha) == 1: pha = pha[0] out = self(pha, **kwargs) if normalized is not None: norm = self(normalized, **kwargs) if self._unit == 'ref': if normalized is not None: out /= norm else: if normalized is None: if self.radius is None: raise ValueError( 'Cannot calculate phase function in reflectance unit' ' because the size of object is unknown. Normalized' ' phase function can be calculated.') if self.wfb is None: raise ValueError('Wavelength/Frequency/Band is unknown.') out = out.to('1/sr', reflectance(self.wfb, cross_section=np.pi*self.radius**2)) else: out = out - norm out = out.to('', u.logarithmic()) if append_results: name = 'ref' i = 1 while name in eph.field_names: name = 'ref'+str(i) i += 1 eph.table.add_column(Column(out, name=name)) return eph else: return out
def to_mag(self, eph, unit=None, append_results=False, **kwargs): """Calculate phase function in magnitude Parameters ---------- eph : `~sbpy.data.Ephem`, numbers, iterables of numbers, or `~astropy.units.Quantity` If `~sbpy.data.Ephem` or dict_like, ephemerides of the object that can include phase angle, heliocentric and geocentric distances via keywords `phase`, `r` and `delta`. If float or array_like, then the phase angle of object. If any distance (heliocentric and geocentric) is not provided, then it will be assumed to be 1 au. If no unit is provided via type `~astropy.units.Quantity`, then radians is assumed for phase angle, and au is assumed for distances. unit : `astropy.units.mag`, `astropy.units.MagUnit`, optional The unit of output magnitude. The corresponding solar magnitude must be available either through `~sbpy.calib.sun` module or set by `~sbpy.calib.solar_fluxd.set`. append_results : bool, optional Controls the return of this method. **kwargs : optional parameters accepted by `astropy.modeling.Model.__call__` Returns ------- `~astropy.units.Quantity`, array if ``append_results == False`` `~sbpy.data.Ephem` if ``append_results == True`` When ``append_results == False``: The calculated magnitude will be returned. When ``append_results == True``: If ``eph`` is a `~sbpy.data.Ephem` object, then the calculated magnitude will be appended to ``eph`` as a new column. Otherwise a new `~sbpy.data.Ephem` object is created to contain the input ``eph`` and the calculated magnitude in two columns. Examples -------- >>> import numpy as np >>> from astropy import units as u >>> from sbpy.photometry import HG >>> from sbpy.data import Ephem >>> ceres_hg = HG(3.34 * u.mag, 0.12) >>> # parameter `eph` as `~sbpy.data.Ephem` type >>> eph = Ephem.from_dict({'alpha': np.linspace(0,np.pi*0.9,200)*u.rad, ... 'r': np.repeat(2.7*u.au, 200), ... 'delta': np.repeat(1.8*u.au, 200)}) >>> mag1 = ceres_hg.to_mag(eph) >>> # parameter `eph` as numpy array >>> pha = np.linspace(0, 170, 200) * u.deg >>> mag2 = ceres_hg.to_mag(pha) """ self._check_unit() pha = eph['alpha'] if len(pha) == 1: pha = pha[0] out = self(pha, **kwargs) if self._unit == 'ref': if unit is None: raise ValueError('Magnitude unit is not specified.') if self.radius is None: raise ValueError( 'Cannot calculate phase function in magnitude because the' ' size of object is unknown.') if self.wfb is None: raise ValueError('Wavelength/Frequency/Band is unknown.') out = out.to(unit, reflectance(self.wfb, cross_section=np.pi * self.radius**2)) dist_corr = self._distance_module(eph) dist_corr = u.Quantity(dist_corr).to(u.mag, u.logarithmic()) out = out - dist_corr if append_results: name = 'mag' i = 1 while name in eph.field_names: name = 'mag'+str(i) i += 1 eph.table.add_column(Column(out, name=name)) return eph else: return out
def from_obs(cls, obs, fitter, fields='mag', init=None, **kwargs): """Instantiate a photometric model class object from data Parameters ---------- obs : `~sbpy.data.DataClass`, dict_like If `~sbpy.data.DataClass` or dict_like, must contain ``'phaseangle'`` or the equivalent names (see `~sbpy.data.DataClass`). If any distance (heliocentric and geocentric) is provided, then they will be used to correct magnitude to 1 au before fitting. fitter : `~astropy.modeling.fitting.Fitter` The fitter to be used for fitting. fields : str or array_like of str The field name or names in ``obs`` to be fitted. If an array_like str, then multiple fields will be fitted one by one and a model set will be returned. In this case, ``.meta['fields']`` of the returned object contains the names of fields fitted. init : numpy array, `~astropy.units.Quantity`, optional The initial parameters for model fitting. Its first dimension has the length of the model parameters, and its second dimension has the length of ``n_model`` if multiple models are fitted. **kwargs : optional parameters accepted by `fitter()`. Note that the magnitude uncertainty can also be supplied to the fit via `weights` keyword for all fitters provided by `~astropy.modeling.fitting`. Returns ------- Object of `DiskIntegratedPhaseFunc` subclass The best-fit model class object. Examples -------- >>> from sbpy.photometry import HG # doctest: +SKIP >>> from sbpy.data import Misc # doctest: +SKIP >>> from astropy.modeling.fitting import LevMarLSQFitter >>> fitter = LevMarLSQFitter() >>> obs = Misc.mpc_observations('Bennu') # doctest: +SKIP >>> hg = HG() # doctest: +SKIP >>> best_hg = hg.from_obs(obs, eph['mag'], fitter) # doctest: +SKIP """ pha = obs['alpha'] if isinstance(fields, (str, bytes)): n_models = 1 else: n_models = len(fields) if init is not None: init = np.asanyarray(init) dist_corr = cls()._distance_module(obs) if n_models == 1: mag = obs[fields] if isinstance(mag, u.Quantity): dist_corr = u.Quantity(dist_corr).to(u.mag, u.logarithmic()) else: dist_corr = -2.5 * alog10(dist_corr) mag0 = mag + dist_corr if init is None: m0 = cls() else: m0 = cls(*init) return fitter(m0, pha, mag0, **kwargs) else: if init is not None: sz1 = init.shape sz2 = len(cls.param_names), n_models if sz1 != sz2: raise ValueError('`init` must have a shape of ({}, {}),' ' shape {} is given.'.format(sz2[0], sz2[1], sz1)) par = np.zeros((len(cls.param_names), n_models)) for i in range(n_models): mag = obs[fields[i]] if isinstance(mag, u.Quantity): dist_corr1 = u.Quantity(dist_corr).to(u.mag, u.logarithmic()) else: dist_corr1 = -2.5 * alog10(dist_corr) mag0 = mag + dist_corr1 if init is None: m0 = cls() else: m0 = cls(*init[:, i]) m = fitter(m0, pha, mag0, **kwargs) par[:, i] = m.parameters pars_list = [] for i, p_name in enumerate(cls.param_names): p = getattr(m, p_name) if p.unit is None: pars_list.append(par[i]) else: pars_list.append(par[i]*p.unit) model = cls(*pars_list, n_models=n_models) if not isinstance(model.meta, dict): model.meta = OrderedDict() model.meta['fields'] = fields return model
def reflectance(wfb, cross_section=None, reflectance=None, **kwargs): """Reflectance related equivalencies. Supports conversion from/to reflectance and scattering cross-section to/from total flux or magnitude at 1 au for both heliocentric and observer distances. Uses `sbpy`'s photometric calibration system: `~sbpy.calib.solar_spectrum` and `~sbpy.calib.solar_fluxd`. Spectral flux density equivalencies for Vega are automatically used, if possible. Dimensionless logarithmic units are also supported if the corresponding solar value is set by `~sbpy.calib.solar_fluxd.set`. Parameters ---------- wfb : `astropy.units.Quantity`, `synphot.SpectralElement`, string Wavelength, frequency, or a bandpass corresponding to the flux density being converted. cross_section : `astropy.units.Qauntity`, optional Total scattering cross-section. One of `cross_section` or `reflectance` is required. reflectance : `astropy.units.Quantity`, optional Average reflectance. One of `cross_section` or `reflectance` is required. **kwargs Keyword arguments for `~Sun.observe()`. Returns ------- equiv : list List of equivalencies Examples -------- Convertion between scattering cross-section and reflectance >>> import numpy as np >>> from astropy import units as u >>> from sbpy.units import reflectance, VEGAmag, spectral_density_vega >>> from sbpy.calib import solar_fluxd, vega_fluxd >>> >>> solar_fluxd.set({'V': -26.77471503 * VEGAmag}) ... # doctest: +IGNORE_OUTPUT >>> vega_fluxd.set({'V': 3.5885e-08 * u.Unit('W / (m2 um)')}) ... # doctest: +IGNORE_OUTPUT >>> mag = 3.4 * VEGAmag >>> cross_sec = np.pi * (460 * u.km)**2 >>> ref = mag.to('1/sr', reflectance('V', cross_section=cross_sec)) >>> print('{0:.4f}'.format(ref)) 0.0287 1 / sr >>> mag1 = ref.to(VEGAmag, reflectance('V', cross_section=cross_sec)) >>> print('{0:.2f}'.format(mag1)) 3.40 mag(VEGA) >>> # Convertion between magnitude and scattering cross-section >>> ref = 0.0287 / u.sr >>> cross_sec = mag.to('km2', reflectance('V', reflectance=ref)) >>> radius = np.sqrt(cross_sec/np.pi) >>> print('{0:.2f}'.format(radius)) 459.69 km >>> mag2 = cross_sec.to(VEGAmag, reflectance('V', reflectance=ref)) >>> print('{0:.2f}'.format(mag2)) 3.40 mag(VEGA) """ # Solar flux density at 1 au in different units f_sun = [] sun = Sun.from_default() for unit in ('W/(m2 um)', 'W/(m2 Hz)', VEGA): try: f_sun.append(sun.observe(wfb, unit=unit, **kwargs)) except SinglePointSpectrumError: f_sun.append(sun(wfb, unit=unit)) except (u.UnitConversionError, FilterLookupError): pass if len(f_sun) == 0: try: f_sun.append(sun.observe(wfb, **kwargs)) except (SinglePointSpectrumError, u.UnitConversionError, FilterLookupError): pass # pass fluxd0 as an optional argument to dereference it, # otherwise both equivalencies will use the fluxd0 for # the last item in f_sun equiv = [] if cross_section is not None: xsec = cross_section.to('au2').value for fluxd0 in f_sun: if fluxd0.unit in [u.mag, u.dB, u.dex]: equiv.append( (fluxd0.unit, u.sr**-1, lambda mag, mag0=fluxd0.value: u .Quantity(mag - mag0, fluxd0.unit).to( '', u.logarithmic()).value / xsec, lambda ref, mag0=fluxd0.value: u.Quantity(ref * xsec).to( fluxd0.unit, u.logarithmic()).value + mag0)) else: equiv.append( (fluxd0.unit, u.sr**-1, lambda fluxd, fluxd0=fluxd0.value: fluxd / (fluxd0 * xsec), lambda ref, fluxd0=fluxd0.value: ref * fluxd0 * xsec)) elif reflectance is not None: ref = reflectance.to('1/sr').value au2km = (const.au.to('km')**2).value for fluxd0 in f_sun: if fluxd0.unit in [u.mag, u.dB, u.dex]: equiv.append( (fluxd0.unit, u.km**2, lambda mag, mag0=fluxd0.value: u .Quantity(mag - mag0, fluxd0.unit).to( '', u.logarithmic()).value / ref * au2km, lambda xsec, mag0=fluxd0.value: u.Quantity(ref * xsec / au2km).to( fluxd0.unit, u.logarithmic()).value + mag0)) else: equiv.append( (fluxd0.unit, u.km**2, lambda fluxd, fluxd0=fluxd0.value: (fluxd / (fluxd0 * ref) * au2km), lambda xsec, fluxd0=fluxd0.value: (fluxd0 * ref * xsec / au2km))) return equiv
def test_multiplication_division(self): """Check that multiplication/division with other units is only possible when the physical unit is dimensionless, and that this turns the unit into a normal one.""" lu1 = u.mag(u.Jy) with pytest.raises(u.UnitsError): lu1 * u.m with pytest.raises(u.UnitsError): u.m * lu1 with pytest.raises(u.UnitsError): lu1 / lu1 for unit in (u.dimensionless_unscaled, u.m, u.mag, u.dex): with pytest.raises(u.UnitsError): lu1 / unit lu2 = u.mag(u.dimensionless_unscaled) with pytest.raises(u.UnitsError): lu2 * lu1 with pytest.raises(u.UnitsError): lu2 / lu1 # But dimensionless_unscaled can be cancelled. assert lu2 / lu2 == u.dimensionless_unscaled # With dimensionless, normal units are OK, but we return a plain unit. tf = lu2 * u.m tr = u.m * lu2 for t in (tf, tr): assert not isinstance(t, type(lu2)) assert t == lu2.function_unit * u.m with u.set_enabled_equivalencies(u.logarithmic()): with pytest.raises(u.UnitsError): t.to(lu2.physical_unit) # Now we essentially have a LogUnit with a prefactor of 100, # so should be equivalent again. t = tf / u.cm with u.set_enabled_equivalencies(u.logarithmic()): assert t.is_equivalent(lu2.function_unit) assert_allclose( t.to(u.dimensionless_unscaled, np.arange(3.) / 100.), lu2.to(lu2.physical_unit, np.arange(3.))) # If we effectively remove lu1, a normal unit should be returned. t2 = tf / lu2 assert not isinstance(t2, type(lu2)) assert t2 == u.m t3 = tf / lu2.function_unit assert not isinstance(t3, type(lu2)) assert t3 == u.m # For completeness, also ensure non-sensical operations fail with pytest.raises(TypeError): lu1 * object() with pytest.raises(TypeError): slice(None) * lu1 with pytest.raises(TypeError): lu1 / [] with pytest.raises(TypeError): 1 / lu1