class Cosmology(metaclass=abc.ABCMeta): """Base-class for all Cosmologies. Parameters ---------- *args Arguments into the cosmology; used by subclasses, not this base class. name : str or None (optional, keyword-only) The name of the cosmology. meta : dict or None (optional, keyword-only) Metadata for the cosmology, e.g., a reference. **kwargs Arguments into the cosmology; used by subclasses, not this base class. Notes ----- Class instances are static -- you cannot (and should not) change the values of the parameters. That is, all of the above attributes (except meta) are read only. For details on how to create performant custom subclasses, see the documentation on :ref:`astropy-cosmology-fast-integrals`. """ meta = MetaData() # Unified I/O object interchange methods from_format = UnifiedReadWriteMethod(CosmologyFromFormat) to_format = UnifiedReadWriteMethod(CosmologyToFormat) # Unified I/O read and write methods read = UnifiedReadWriteMethod(CosmologyRead) write = UnifiedReadWriteMethod(CosmologyWrite) # Parameters __parameters__ = () __all_parameters__ = () # --------------------------------------------------------------- def __init_subclass__(cls): super().__init_subclass__() # ------------------- # Parameters # Get parameters that are still Parameters, either in this class or above. parameters = [] derived_parameters = [] for n in cls.__parameters__: p = getattr(cls, n) if isinstance(p, Parameter): derived_parameters.append( n) if p.derived else parameters.append(n) # Add new parameter definitions for n, v in cls.__dict__.items(): if n in parameters or n.startswith("_") or not isinstance( v, Parameter): continue derived_parameters.append(n) if v.derived else parameters.append(n) # reorder to match signature ordered = [ parameters.pop(parameters.index(n)) for n in cls._init_signature.parameters.keys() if n in parameters ] parameters = ordered + parameters # place "unordered" at the end cls.__parameters__ = tuple(parameters) cls.__all_parameters__ = cls.__parameters__ + tuple(derived_parameters) # ------------------- # register as a Cosmology subclass _COSMOLOGY_CLASSES[cls.__qualname__] = cls @classproperty(lazy=True) def _init_signature(cls): """Initialization signature (without 'self').""" # get signature, dropping "self" by taking arguments [1:] sig = inspect.signature(cls.__init__) sig = sig.replace(parameters=list(sig.parameters.values())[1:]) return sig # --------------------------------------------------------------- def __init__(self, name=None, meta=None): self._name = str(name) if name is not None else name self.meta.update(meta or {}) @property def name(self): """The name of the Cosmology instance.""" return self._name @property @abc.abstractmethod def is_flat(self): """ Return bool; `True` if the cosmology is flat. This is abstract and must be defined in subclasses. """ raise NotImplementedError("is_flat is not implemented") def clone(self, *, meta=None, **kwargs): """Returns a copy of this object with updated parameters, as specified. This cannot be used to change the type of the cosmology, so ``clone()`` cannot be used to change between flat and non-flat cosmologies. Parameters ---------- meta : mapping or None (optional, keyword-only) Metadata that will update the current metadata. **kwargs Cosmology parameter (and name) modifications. If any parameter is changed and a new name is not given, the name will be set to "[old name] (modified)". Returns ------- newcosmo : `~astropy.cosmology.Cosmology` subclass instance A new instance of this class with updated parameters as specified. If no arguments are given, then a reference to this object is returned instead of copy. Examples -------- To make a copy of the ``Planck13`` cosmology with a different matter density (``Om0``), and a new name: >>> from astropy.cosmology import Planck13 >>> Planck13.clone(name="Modified Planck 2013", Om0=0.35) FlatLambdaCDM(name="Modified Planck 2013", H0=67.77 km / (Mpc s), Om0=0.35, ... If no name is specified, the new name will note the modification. >>> Planck13.clone(Om0=0.35).name 'Planck13 (modified)' """ # Quick return check, taking advantage of the Cosmology immutability. if meta is None and not kwargs: return self # There are changed parameter or metadata values. # The name needs to be changed accordingly, if it wasn't already. _modname = self.name + " (modified)" kwargs.setdefault("name", (_modname if self.name is not None else None)) # mix new meta into existing, preferring the former. meta = meta if meta is not None else {} new_meta = {**self.meta, **meta} # Mix kwargs into initial arguments, preferring the former. new_init = {**self._init_arguments, "meta": new_meta, **kwargs} # Create BoundArgument to handle args versus kwargs. # This also handles all errors from mismatched arguments ba = self._init_signature.bind_partial(**new_init) # Instantiate, respecting args vs kwargs cloned = type(self)(*ba.args, **ba.kwargs) # Check if nothing has changed. # TODO! or should return self? if (cloned.name == _modname) and not meta and cloned.is_equivalent(self): cloned._name = self.name return cloned @property def _init_arguments(self): # parameters kw = {n: getattr(self, n) for n in self.__parameters__} # other info kw["name"] = self.name kw["meta"] = self.meta return kw # --------------------------------------------------------------- # comparison methods def is_equivalent(self, other, *, format=False): r"""Check equivalence between Cosmologies. Two cosmologies may be equivalent even if not the same class. For example, an instance of ``LambdaCDM`` might have :math:`\Omega_0=1` and :math:`\Omega_k=0` and therefore be flat, like ``FlatLambdaCDM``. Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance The object in which to compare. format : bool or None or str, optional keyword-only Whether to allow, before equivalence is checked, the object to be converted to a |Cosmology|. This allows, e.g. a |Table| to be equivalent to a Cosmology. `False` (default) will not allow conversion. `True` or `None` will, and will use the auto-identification to try to infer the correct format. A `str` is assumed to be the correct format to use when converting. Returns ------- bool True if cosmologies are equivalent, False otherwise. Examples -------- Two cosmologies may be equivalent even if not of the same class. In this examples the ``LambdaCDM`` has ``Ode0`` set to the same value calculated in ``FlatLambdaCDM``. >>> import astropy.units as u >>> from astropy.cosmology import LambdaCDM, FlatLambdaCDM >>> cosmo1 = LambdaCDM(70 * (u.km/u.s/u.Mpc), 0.3, 0.7) >>> cosmo2 = FlatLambdaCDM(70 * (u.km/u.s/u.Mpc), 0.3) >>> cosmo1.is_equivalent(cosmo2) True While in this example, the cosmologies are not equivalent. >>> cosmo3 = FlatLambdaCDM(70 * (u.km/u.s/u.Mpc), 0.3, Tcmb0=3 * u.K) >>> cosmo3.is_equivalent(cosmo2) False Also, using the keyword argument, the notion of equivalence is extended to any Python object that can be converted to a |Cosmology|. >>> from astropy.cosmology import Planck18 >>> tbl = Planck18.to_format("astropy.table") >>> Planck18.is_equivalent(tbl, format=True) True The list of valid formats, e.g. the |Table| in this example, may be checked with ``Cosmology.from_format.list_formats()``. As can be seen in the list of formats, not all formats can be auto-identified by ``Cosmology.from_format.registry``. Objects of these kinds can still be checked for equivalence, but the correct format string must be used. >>> tbl = Planck18.to_format("yaml") >>> Planck18.is_equivalent(tbl, format="yaml") True """ # Allow for different formats to be considered equivalent. if format is not False: format = None if format is True else format # str->str, None/True->None try: other = Cosmology.from_format(other, format=format) except Exception: # TODO! should enforce only TypeError return False # The options are: 1) same class & parameters; 2) same class, different # parameters; 3) different classes, equivalent parameters; 4) different # classes, different parameters. (1) & (3) => True, (2) & (4) => False. equiv = self.__equiv__(other) if equiv is NotImplemented and hasattr(other, "__equiv__"): equiv = other.__equiv__(self) # that failed, try from 'other' return equiv if equiv is not NotImplemented else False def __equiv__(self, other): """Cosmology equivalence. Use ``.is_equivalent()`` for actual check! Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance The object in which to compare. Returns ------- bool or `NotImplemented` `NotImplemented` if 'other' is from a different class. `True` if 'other' is of the same class and has matching parameters and parameter values. `False` otherwise. """ if other.__class__ is not self.__class__: return NotImplemented # allows other.__equiv__ # check all parameters in 'other' match those in 'self' and 'other' has # no extra parameters (latter part should never happen b/c same class) params_eq = (set(self.__all_parameters__) == set( other.__all_parameters__) and all( np.all(getattr(self, k) == getattr(other, k)) for k in self.__all_parameters__)) return params_eq def __eq__(self, other): """Check equality between Cosmologies. Checks the Parameters and immutable fields (i.e. not "meta"). Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance The object in which to compare. Returns ------- bool `True` if Parameters and names are the same, `False` otherwise. """ if other.__class__ is not self.__class__: return NotImplemented # allows other.__eq__ # check all parameters in 'other' match those in 'self' equivalent = self.__equiv__(other) # non-Parameter checks: name name_eq = (self.name == other.name) return equivalent and name_eq # --------------------------------------------------------------- def __repr__(self): namelead = f"{self.__class__.__qualname__}(" if self.name is not None: namelead += f"name=\"{self.name}\", " # nicely formatted parameters fmtps = (f'{k}={getattr(self, k)}' for k in self.__parameters__) return namelead + ", ".join(fmtps) + ")" def __astropy_table__(self, cls, copy, **kwargs): """Return a `~astropy.table.Table` of type ``cls``. Parameters ---------- cls : type Astropy ``Table`` class or subclass. copy : bool Ignored. **kwargs : dict, optional Additional keyword arguments. Passed to ``self.to_format()``. See ``Cosmology.to_format.help("astropy.table")`` for allowed kwargs. Returns ------- `astropy.table.Table` or subclass instance Instance of type ``cls``. """ return self.to_format("astropy.table", cls=cls, **kwargs)
class Cosmology(metaclass=abc.ABCMeta): """Base-class for all Cosmologies. Parameters ---------- *args Arguments into the cosmology; used by subclasses, not this base class. name : str or None (optional, keyword-only) The name of the cosmology. meta : dict or None (optional, keyword-only) Metadata for the cosmology, e.g., a reference. **kwargs Arguments into the cosmology; used by subclasses, not this base class. Notes ----- Class instances are static -- you cannot (and should not) change the values of the parameters. That is, all of the above attributes (except meta) are read only. For details on how to create performant custom subclasses, see the documentation on :ref:`astropy-cosmology-fast-integrals`. """ meta = MetaData() # Unified I/O object interchange methods from_format = UnifiedReadWriteMethod(CosmologyFromFormat) to_format = UnifiedReadWriteMethod(CosmologyToFormat) # Unified I/O read and write methods read = UnifiedReadWriteMethod(CosmologyRead) write = UnifiedReadWriteMethod(CosmologyWrite) # Parameters __parameters__: tuple[str, ...] = () __all_parameters__: tuple[str, ...] = () # --------------------------------------------------------------- def __init_subclass__(cls): super().__init_subclass__() # ------------------- # Parameters # Get parameters that are still Parameters, either in this class or above. parameters = [] derived_parameters = [] for n in cls.__parameters__: p = getattr(cls, n) if isinstance(p, Parameter): derived_parameters.append( n) if p.derived else parameters.append(n) # Add new parameter definitions for n, v in cls.__dict__.items(): if n in parameters or n.startswith("_") or not isinstance( v, Parameter): continue derived_parameters.append(n) if v.derived else parameters.append(n) # reorder to match signature ordered = [ parameters.pop(parameters.index(n)) for n in cls._init_signature.parameters.keys() if n in parameters ] parameters = ordered + parameters # place "unordered" at the end cls.__parameters__ = tuple(parameters) cls.__all_parameters__ = cls.__parameters__ + tuple(derived_parameters) # ------------------- # register as a Cosmology subclass _COSMOLOGY_CLASSES[cls.__qualname__] = cls @classproperty(lazy=True) def _init_signature(cls): """Initialization signature (without 'self').""" # get signature, dropping "self" by taking arguments [1:] sig = inspect.signature(cls.__init__) sig = sig.replace(parameters=list(sig.parameters.values())[1:]) return sig # --------------------------------------------------------------- def __init__(self, name=None, meta=None): self._name = str(name) if name is not None else name self.meta.update(meta or {}) @property def name(self): """The name of the Cosmology instance.""" return self._name @property @abc.abstractmethod def is_flat(self): """ Return bool; `True` if the cosmology is flat. This is abstract and must be defined in subclasses. """ raise NotImplementedError("is_flat is not implemented") def clone(self, *, meta=None, **kwargs): """Returns a copy of this object with updated parameters, as specified. This cannot be used to change the type of the cosmology, so ``clone()`` cannot be used to change between flat and non-flat cosmologies. Parameters ---------- meta : mapping or None (optional, keyword-only) Metadata that will update the current metadata. **kwargs Cosmology parameter (and name) modifications. If any parameter is changed and a new name is not given, the name will be set to "[old name] (modified)". Returns ------- newcosmo : `~astropy.cosmology.Cosmology` subclass instance A new instance of this class with updated parameters as specified. If no arguments are given, then a reference to this object is returned instead of copy. Examples -------- To make a copy of the ``Planck13`` cosmology with a different matter density (``Om0``), and a new name: >>> from astropy.cosmology import Planck13 >>> Planck13.clone(name="Modified Planck 2013", Om0=0.35) FlatLambdaCDM(name="Modified Planck 2013", H0=67.77 km / (Mpc s), Om0=0.35, ... If no name is specified, the new name will note the modification. >>> Planck13.clone(Om0=0.35).name 'Planck13 (modified)' """ # Quick return check, taking advantage of the Cosmology immutability. if meta is None and not kwargs: return self # There are changed parameter or metadata values. # The name needs to be changed accordingly, if it wasn't already. _modname = self.name + " (modified)" kwargs.setdefault("name", (_modname if self.name is not None else None)) # mix new meta into existing, preferring the former. meta = meta if meta is not None else {} new_meta = {**self.meta, **meta} # Mix kwargs into initial arguments, preferring the former. new_init = {**self._init_arguments, "meta": new_meta, **kwargs} # Create BoundArgument to handle args versus kwargs. # This also handles all errors from mismatched arguments ba = self._init_signature.bind_partial(**new_init) # Instantiate, respecting args vs kwargs cloned = type(self)(*ba.args, **ba.kwargs) # Check if nothing has changed. # TODO! or should return self? if (cloned.name == _modname) and not meta and cloned.is_equivalent(self): cloned._name = self.name return cloned @property def _init_arguments(self): # parameters kw = {n: getattr(self, n) for n in self.__parameters__} # other info kw["name"] = self.name kw["meta"] = self.meta return kw # --------------------------------------------------------------- # comparison methods def is_equivalent(self, other: Any, /, *, format: _FormatType = False) -> bool: r"""Check equivalence between Cosmologies. Two cosmologies may be equivalent even if not the same class. For example, an instance of ``LambdaCDM`` might have :math:`\Omega_0=1` and :math:`\Omega_k=0` and therefore be flat, like ``FlatLambdaCDM``. Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance, positional-only The object to which to compare. format : bool or None or str, optional keyword-only Whether to allow, before equivalence is checked, the object to be converted to a |Cosmology|. This allows, e.g. a |Table| to be equivalent to a Cosmology. `False` (default) will not allow conversion. `True` or `None` will, and will use the auto-identification to try to infer the correct format. A `str` is assumed to be the correct format to use when converting. ``format`` is broadcast to match the shape of ``other``. Note that the cosmology arguments are not broadcast against ``format``, so it cannot determine the output shape. Returns ------- bool True if cosmologies are equivalent, False otherwise. Examples -------- Two cosmologies may be equivalent even if not of the same class. In this examples the ``LambdaCDM`` has ``Ode0`` set to the same value calculated in ``FlatLambdaCDM``. >>> import astropy.units as u >>> from astropy.cosmology import LambdaCDM, FlatLambdaCDM >>> cosmo1 = LambdaCDM(70 * (u.km/u.s/u.Mpc), 0.3, 0.7) >>> cosmo2 = FlatLambdaCDM(70 * (u.km/u.s/u.Mpc), 0.3) >>> cosmo1.is_equivalent(cosmo2) True While in this example, the cosmologies are not equivalent. >>> cosmo3 = FlatLambdaCDM(70 * (u.km/u.s/u.Mpc), 0.3, Tcmb0=3 * u.K) >>> cosmo3.is_equivalent(cosmo2) False Also, using the keyword argument, the notion of equivalence is extended to any Python object that can be converted to a |Cosmology|. >>> from astropy.cosmology import Planck18 >>> tbl = Planck18.to_format("astropy.table") >>> Planck18.is_equivalent(tbl, format=True) True The list of valid formats, e.g. the |Table| in this example, may be checked with ``Cosmology.from_format.list_formats()``. As can be seen in the list of formats, not all formats can be auto-identified by ``Cosmology.from_format.registry``. Objects of these kinds can still be checked for equivalence, but the correct format string must be used. >>> tbl = Planck18.to_format("yaml") >>> Planck18.is_equivalent(tbl, format="yaml") True """ from .funcs import cosmology_equal try: return cosmology_equal(self, other, format=(None, format), allow_equivalent=True) except Exception: # `is_equivalent` allows `other` to be any object and returns False # if `other` cannot be converted to a Cosmology, rather than # raising an Exception. return False
class Cosmology(metaclass=abc.ABCMeta): """Base-class for all Cosmologies. Parameters ---------- *args Arguments into the cosmology; used by subclasses, not this base class. name : str or None (optional, keyword-only) The name of the cosmology. meta : dict or None (optional, keyword-only) Metadata for the cosmology, e.g., a reference. **kwargs Arguments into the cosmology; used by subclasses, not this base class. Notes ----- Class instances are static -- you cannot (and should not) change the values of the parameters. That is, all of the above attributes (except meta) are read only. For details on how to create performant custom subclasses, see the documentation on :ref:`astropy-cosmology-fast-integrals`. """ meta = MetaData() # Unified I/O object interchange methods from_format = UnifiedReadWriteMethod(CosmologyFromFormat) to_format = UnifiedReadWriteMethod(CosmologyToFormat) # Unified I/O read and write methods read = UnifiedReadWriteMethod(CosmologyRead) write = UnifiedReadWriteMethod(CosmologyWrite) # Parameters __parameters__ = () __all_parameters__ = () # --------------------------------------------------------------- def __init_subclass__(cls): super().__init_subclass__() # ------------------- # Parameters # Get parameters that are still Parameters, either in this class or above. parameters = [] derived_parameters = [] for n in cls.__parameters__: p = getattr(cls, n) if isinstance(p, Parameter): derived_parameters.append( n) if p.derived else parameters.append(n) # Add new parameter definitions for n, v in cls.__dict__.items(): if n in parameters or n.startswith("_") or not isinstance( v, Parameter): continue derived_parameters.append(n) if v.derived else parameters.append(n) # reorder to match signature ordered = [ parameters.pop(parameters.index(n)) for n in cls._init_signature.parameters.keys() if n in parameters ] parameters = ordered + parameters # place "unordered" at the end cls.__parameters__ = tuple(parameters) cls.__all_parameters__ = cls.__parameters__ + tuple(derived_parameters) # ------------------- # register as a Cosmology subclass _COSMOLOGY_CLASSES[cls.__qualname__] = cls @classproperty(lazy=True) def _init_signature(cls): """Initialization signature (without 'self').""" # get signature, dropping "self" by taking arguments [1:] sig = inspect.signature(cls.__init__) sig = sig.replace(parameters=list(sig.parameters.values())[1:]) return sig # --------------------------------------------------------------- def __init__(self, name=None, meta=None): self._name = name self.meta.update(meta or {}) @property def name(self): """The name of the Cosmology instance.""" return self._name @property @abc.abstractmethod def is_flat(self): """ Return bool; `True` if the cosmology is flat. This is abstract and must be defined in subclasses. """ raise NotImplementedError("is_flat is not implemented") def clone(self, *, meta=None, **kwargs): """Returns a copy of this object with updated parameters, as specified. This cannot be used to change the type of the cosmology, so ``clone()`` cannot be used to change between flat and non-flat cosmologies. Parameters ---------- meta : mapping or None (optional, keyword-only) Metadata that will update the current metadata. **kwargs Cosmology parameter (and name) modifications. If any parameter is changed and a new name is not given, the name will be set to "[old name] (modified)". Returns ------- newcosmo : `~astropy.cosmology.Cosmology` subclass instance A new instance of this class with updated parameters as specified. If no modifications are requested, then a reference to this object is returned instead of copy. Examples -------- To make a copy of the ``Planck13`` cosmology with a different matter density (``Om0``), and a new name: >>> from astropy.cosmology import Planck13 >>> newcosmo = Planck13.clone(name="Modified Planck 2013", Om0=0.35) If no name is specified, the new name will note the modification. >>> Planck13.clone(Om0=0.35).name 'Planck13 (modified)' """ # Quick return check, taking advantage of the Cosmology immutability. if meta is None and not kwargs: return self # There are changed parameter or metadata values. # The name needs to be changed accordingly, if it wasn't already. kwargs.setdefault( "name", (self.name + " (modified)" if self.name is not None else None)) # mix new meta into existing, preferring the former. new_meta = {**self.meta, **(meta or {})} # Mix kwargs into initial arguments, preferring the former. new_init = {**self._init_arguments, "meta": new_meta, **kwargs} # Create BoundArgument to handle args versus kwargs. # This also handles all errors from mismatched arguments ba = self._init_signature.bind_partial(**new_init) # Return new instance, respecting args vs kwargs return self.__class__(*ba.args, **ba.kwargs) @property def _init_arguments(self): # parameters kw = {n: getattr(self, n) for n in self.__parameters__} # other info kw["name"] = self.name kw["meta"] = self.meta return kw # --------------------------------------------------------------- # comparison methods def is_equivalent(self, other): r"""Check equivalence between Cosmologies. Two cosmologies may be equivalent even if not the same class. For example, an instance of ``LambdaCDM`` might have :math:`\Omega_0=1` and :math:`\Omega_k=0` and therefore be flat, like ``FlatLambdaCDM``. Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance The object in which to compare. Returns ------- bool True if cosmologies are equivalent, False otherwise. """ # The options are: 1) same class & parameters; 2) same class, different # parameters; 3) different classes, equivalent parameters; 4) different # classes, different parameters. (1) & (3) => True, (2) & (4) => False. equiv = self.__equiv__(other) if equiv is NotImplemented and hasattr(other, "__equiv__"): equiv = other.__equiv__(self) # that failed, try from 'other' return equiv if equiv is not NotImplemented else False def __equiv__(self, other): """Cosmology equivalence. Use ``.is_equivalent()`` for actual check! Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance The object in which to compare. Returns ------- bool or `NotImplemented` `NotImplemented` if 'other' is from a different class. `True` if 'other' is of the same class and has matching parameters and parameter values. `False` otherwise. """ if other.__class__ is not self.__class__: return NotImplemented # allows other.__equiv__ # check all parameters in 'other' match those in 'self' and 'other' has # no extra parameters (latter part should never happen b/c same class) params_eq = (set(self.__all_parameters__) == set( other.__all_parameters__) and all( np.all(getattr(self, k) == getattr(other, k)) for k in self.__all_parameters__)) return params_eq def __eq__(self, other): """Check equality between Cosmologies. Checks the Parameters and immutable fields (i.e. not "meta"). Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance The object in which to compare. Returns ------- bool True if Parameters and names are the same, False otherwise. """ if other.__class__ is not self.__class__: return NotImplemented # allows other.__eq__ # check all parameters in 'other' match those in 'self' equivalent = self.__equiv__(other) # non-Parameter checks: name name_eq = (self.name == other.name) return equivalent and name_eq # --------------------------------------------------------------- def __repr__(self): ps = {k: getattr(self, k) for k in self.__parameters__} # values cps = {k: getattr(self.__class__, k) for k in self.__parameters__} # Parameter objects namelead = f"{self.__class__.__qualname__}(" if self.name is not None: namelead += f"name=\"{self.name}\", " # nicely formatted parameters fmtps = (k + '=' + format(v, cps[k].format_spec if v is not None else '') for k, v in ps.items()) return namelead + ", ".join(fmtps) + ")" def __astropy_table__(self, cls, copy, **kwargs): """Return a `~astropy.table.Table` of type ``cls``. Parameters ---------- cls : type Astropy ``Table`` class or subclass. copy : bool Ignored. **kwargs : dict, optional Additional keyword arguments. Passed to ``self.to_format()``. See ``Cosmology.to_format.help("astropy.table")`` for allowed kwargs. Returns ------- `astropy.table.Table` or subclass instance Instance of type ``cls``. """ return self.to_format("astropy.table", cls=cls, **kwargs)
class StokesSpectralCube(object): """ A class to store a spectral cube with multiple Stokes parameters. The individual Stokes cubes can share a common mask in addition to having component-specific masks. """ def __init__(self, stokes_data, mask=None, meta=None, fill_value=None): self._stokes_data = stokes_data self._meta = meta or {} self._fill_value = fill_value reference = tuple(stokes_data.keys())[0] for component in stokes_data: if not isinstance(stokes_data[component], BaseSpectralCube): raise TypeError("stokes_data should be a dictionary of " "SpectralCube objects") if not wcs_utils.check_equality(stokes_data[component].wcs, stokes_data[reference].wcs): raise ValueError("All spectral cubes in stokes_data " "should have the same WCS") if component not in VALID_STOKES: raise ValueError("Invalid Stokes component: {0} - should be " "one of I, Q, U, V, RR, LL, RL, LR".format(component)) if stokes_data[component].shape != stokes_data[reference].shape: raise ValueError("All spectral cubes should have the same shape") self._wcs = stokes_data[reference].wcs self._shape = stokes_data[reference].shape if isinstance(mask, BooleanArrayMask): if not is_broadcastable_and_smaller(mask.shape, self._shape): raise ValueError("Mask shape is not broadcastable to data shape:" " {0} vs {1}".format(mask.shape, self._shape)) self._mask = mask @property def shape(self): return self._shape @property def mask(self): """ The underlying mask """ return self._mask @property def wcs(self): return self._wcs def __dir__(self): if six.PY2: return self.components + dir(type(self)) + list(self.__dict__) else: return self.components + super(StokesSpectralCube, self).__dir__() @property def components(self): return list(self._stokes_data.keys()) def __getattr__(self, attribute): """ Descriptor to return the Stokes cubes """ if attribute in self._stokes_data: if self.mask is not None: return self._stokes_data[attribute].with_mask(self.mask) else: return self._stokes_data[attribute] else: raise AttributeError("StokesSpectralCube has no attribute {0}".format(attribute)) def with_mask(self, mask, inherit_mask=True): """ Return a new StokesSpectralCube instance that contains a composite mask of the current StokesSpectralCube and the new ``mask``. Parameters ---------- mask : :class:`MaskBase` instance, or boolean numpy array The mask to apply. If a boolean array is supplied, it will be converted into a mask, assuming that `True` values indicate included elements. inherit_mask : bool (optional, default=True) If True, combines the provided mask with the mask currently attached to the cube Returns ------- new_cube : :class:`StokesSpectralCube` A cube with the new mask applied. Notes ----- This operation returns a view into the data, and not a copy. """ if isinstance(mask, np.ndarray): if not is_broadcastable_and_smaller(mask.shape, self.shape): raise ValueError("Mask shape is not broadcastable to data shape: " "%s vs %s" % (mask.shape, self.shape)) mask = BooleanArrayMask(mask, self.wcs) if self._mask is not None: return self._new_cube_with(mask=self.mask & mask if inherit_mask else mask) else: return self._new_cube_with(mask=mask) def _new_cube_with(self, stokes_data=None, mask=None, meta=None, fill_value=None): data = self._stokes_data if stokes_data is None else stokes_data mask = self._mask if mask is None else mask if meta is None: meta = {} meta.update(self._meta) fill_value = self._fill_value if fill_value is None else fill_value cube = StokesSpectralCube(stokes_data=data, mask=mask, meta=meta, fill_value=fill_value) return cube def with_spectral_unit(self, unit, **kwargs): stokes_data = {k: self._stokes_data[k].with_spectral_unit(unit, **kwargs) for k in self._stokes_data} return self._new_cube_with(stokes_data=stokes_data) read = UnifiedReadWriteMethod(StokesSpectralCubeRead) write = UnifiedReadWriteMethod(StokesSpectralCubeWrite)
class LowerDimensionalObject(u.Quantity, BaseNDClass, HeaderMixinClass): """ Generic class for 1D and 2D objects. """ @property def hdu(self): if self.wcs is None: hdu = PrimaryHDU(self.value) else: hdu = PrimaryHDU(self.value, header=self.header) hdu.header['BUNIT'] = self.unit.to_string(format='fits') if 'beam' in self.meta: hdu.header.update(self.meta['beam'].to_header_keywords()) return hdu def read(self, *args, **kwargs): raise NotImplementedError() write = UnifiedReadWriteMethod(LowerDimensionalObjectWrite) def __getslice__(self, start, end, increment=None): # I don't know why this is needed, but apparently one of the inherited # classes implements getslice, which forces us to overwrite it # I can't find any examples where __getslice__ is actually implemented, # though, so this seems like a deep and frightening bug. #log.debug("Getting a slice from {0} to {1}".format(start,end)) return self.__getitem__(slice(start, end, increment)) def __getitem__(self, key, **kwargs): """ Return a new `~spectral_cube.lower_dimensional_structures.LowerDimensionalObject` of the same class while keeping other properties fixed. """ new_qty = super(LowerDimensionalObject, self).__getitem__(key) if new_qty.ndim < 2: # do not return a projection return u.Quantity(new_qty) if self._wcs is not None: if ((isinstance(key, tuple) and any(isinstance(k, slice) for k in key) and len(key) > self.ndim)): # Example cases include: indexing tricks like [:,:,None] warnings.warn("Slice {0} cannot be used on this {1}-dimensional" " array's WCS. If this is intentional, you " " should use this {2}'s ``array`` or ``quantity``" " attribute." .format(key, self.ndim, type(self)), SliceWarning ) return self.quantity[key] else: newwcs = self._wcs[key] else: newwcs = None new = self.__class__(value=new_qty.value, unit=new_qty.unit, copy=False, wcs=newwcs, meta=self._meta, mask=(self._mask[key] if self._mask is not nomask else None), header=self._header, **kwargs) new._wcs = newwcs new._meta = self._meta new._mask=(self._mask[key] if self._mask is not nomask else nomask) new._header = self._header return new def __array_finalize__(self, obj): self._wcs = getattr(obj, '_wcs', None) self._meta = getattr(obj, '_meta', None) self._mask = getattr(obj, '_mask', None) self._header = getattr(obj, '_header', None) self._spectral_unit = getattr(obj, '_spectral_unit', None) self._fill_value = getattr(obj, '_fill_value', np.nan) self._wcs_tolerance = getattr(obj, '_wcs_tolerance', 0.0) if isinstance(obj, VaryingResolutionOneDSpectrum): self._beams = getattr(obj, '_beams', None) else: self._beam = getattr(obj, '_beam', None) super(LowerDimensionalObject, self).__array_finalize__(obj) @property def __array_priority__(self): return super(LowerDimensionalObject, self).__array_priority__*2 @property def array(self): """ Get a pure array representation of the LDO. Useful when multiplying and using numpy indexing tricks. """ return np.asarray(self) @property def _data(self): # the _data property is required by several other mixins # (which probably means defining it here is a bad design) return self.array @property def quantity(self): """ Get a pure `~astropy.units.Quantity` representation of the LDO. """ return u.Quantity(self) def to(self, unit, equivalencies=[], freq=None): """ Return a new `~spectral_cube.lower_dimensional_structures.Projection` of the same class with the specified unit. See `astropy.units.Quantity.to` for further details. """ if not isinstance(unit, u.Unit): unit = u.Unit(unit) if unit == self.unit: # No copying return self if ((self.unit.is_equivalent(u.Jy / u.beam) and not any({u.Jy/u.beam, u.K}.issubset(set(eq)) for eq in equivalencies))): # the 'not any' above checks that there is not already a defined # Jy<->K equivalency. If there is, the code below is redundant # and will cause problems. if hasattr(self, 'beams'): factor = (self.jtok_factors(equivalencies=equivalencies) * (self.unit*u.beam).to(u.Jy)) else: # replace "beam" with the actual beam if not hasattr(self, 'beam'): raise ValueError("To convert objects with Jy/beam units, " "the object needs to have a beam defined.") brightness_unit = self.unit * u.beam # create a beam equivalency for brightness temperature if freq is None: try: freq = self.with_spectral_unit(u.Hz).spectral_axis except AttributeError: raise TypeError("Object of type {0} has no spectral " "information. `freq` must be provided for" " unit conversion from Jy/beam" .format(type(self))) else: if not freq.unit.is_equivalent(u.Hz): raise u.UnitsError("freq must be given in equivalent " "frequency units.") bmequiv = self.beam.jtok_equiv(freq) # backport to handle astropy < 3: the beam equivalency was only # modified to handle jy/beam in astropy 3 if bmequiv[0] == u.Jy: bmequiv.append([u.Jy/u.beam, u.K, bmequiv[2], bmequiv[3]]) factor = brightness_unit.to(unit, equivalencies=bmequiv + list(equivalencies)) else: # scaling factor factor = self.unit.to(unit, equivalencies=equivalencies) converted_array = (self.quantity * factor).value # use private versions of variables, not the generated property # versions # Not entirely sure the use of __class__ here is kosher, but we do want # self.__class__, not super() new = self.__class__(value=converted_array, unit=unit, copy=True, wcs=self._wcs, meta=self._meta, mask=self._mask, header=self._header) return new @property def _mask(self): """ Annoying hack to deal with np.ma.core.is_mask failures (I don't like using __ but I think it's necessary here)""" if self.__mask is None: # need this to be *exactly* the numpy boolean False return nomask return self.__mask @_mask.setter def _mask(self, value): self.__mask = value def shrink_mask(self): """ Copy of the numpy masked_array shrink_mask method. This is essentially a hack needed for matplotlib to show images. """ m = self._mask if m.ndim and not m.any(): self._mask = nomask return self def _initial_set_mask(self, mask): """ Helper tool to validate mask when originally setting it in __new__ Note that because this is intended to be used in __new__, order matters: ``self`` must have ``_wcs``, for example. """ if mask is None: mask = BooleanArrayMask(np.ones_like(self.value, dtype=bool), self._wcs, shape=self.value.shape) elif isinstance(mask, np.ndarray): if mask.shape != self.value.shape: raise ValueError("Mask shape must match the {0} shape." .format(self.__class__.__name__) ) mask = BooleanArrayMask(mask, self._wcs, shape=self.value.shape) elif isinstance(mask, MaskBase): pass else: raise TypeError("mask of type {} is not a supported mask " "type.".format(type(mask))) # Validate the mask before setting mask._validate_wcs(new_data=self.value, new_wcs=self._wcs, wcs_tolerance=self._wcs_tolerance) self._mask = mask
class Cosmology(metaclass=ABCMeta): """Base-class for all Cosmologies. Parameters ---------- *args Arguments into the cosmology; used by subclasses, not this base class. name : str or None (optional, keyword-only) The name of the cosmology. meta : dict or None (optional, keyword-only) Metadata for the cosmology, e.g., a reference. **kwargs Arguments into the cosmology; used by subclasses, not this base class. Notes ----- Class instances are static -- you cannot (and should not) change the values of the parameters. That is, all of the above attributes (except meta) are read only. For details on how to create performant custom subclasses, see the documentation on :ref:`astropy-cosmology-fast-integrals`. """ meta = MetaData() # Unified I/O object interchange methods from_format = UnifiedReadWriteMethod(CosmologyFromFormat) to_format = UnifiedReadWriteMethod(CosmologyToFormat) # Unified I/O read and write methods read = UnifiedReadWriteMethod(CosmologyRead) write = UnifiedReadWriteMethod(CosmologyWrite) def __init_subclass__(cls): super().__init_subclass__() _COSMOLOGY_CLASSES[cls.__qualname__] = cls def __new__(cls, *args, **kwargs): self = super().__new__(cls) # bundle and store initialization arguments on the instance ba = cls._init_signature.bind_partial(*args, **kwargs) ba.apply_defaults() # and fill in the defaults self._init_arguments = ba.arguments return self def __init__(self, *args, name=None, meta=None, **kwargs): self._name = name self.meta.update(meta or {}) @classproperty(lazy=True) def _init_signature(cls): """Initialization signature (without 'self').""" # get signature, dropping "self" by taking arguments [1:] sig = signature(cls.__init__) sig = sig.replace(parameters=list(sig.parameters.values())[1:]) return sig @property def name(self): """The name of the Cosmology instance.""" return self._name def clone(self, *, meta=None, **kwargs): """Returns a copy of this object with updated parameters, as specified. This cannot be used to change the type of the cosmology, so ``clone()`` cannot be used to change between flat and non-flat cosmologies. Parameters ---------- meta : mapping or None (optional, keyword-only) Metadata that will update the current metadata. **kwargs Cosmology parameter (and name) modifications. If any parameter is changed and a new name is not given, the name will be set to "[old name] (modified)". Returns ------- newcosmo : `~astropy.cosmology.Cosmology` subclass instance A new instance of this class with updated parameters as specified. If no modifications are requested, then a reference to this object is returned instead of copy. Examples -------- To make a copy of the ``Planck13`` cosmology with a different matter density (``Om0``), and a new name: >>> from astropy.cosmology import Planck13 >>> newcosmo = Planck13.clone(name="Modified Planck 2013", Om0=0.35) If no name is specified, the new name will note the modification. >>> Planck13.clone(Om0=0.35).name 'Planck13 (modified)' """ # Quick return check, taking advantage of the Cosmology immutability. if meta is None and not kwargs: return self # There are changed parameter or metadata values. # The name needs to be changed accordingly, if it wasn't already. kwargs.setdefault( "name", (self.name + " (modified)" if self.name is not None else None)) # mix new meta into existing, preferring the former. new_meta = {**self.meta, **(meta or {})} # Mix kwargs into initial arguments, preferring the former. new_init = {**self._init_arguments, "meta": new_meta, **kwargs} # Create BoundArgument to handle args versus kwargs. # This also handles all errors from mismatched arguments ba = self._init_signature.bind_partial(**new_init) # Return new instance, respecting args vs kwargs return self.__class__(*ba.args, **ba.kwargs) # ----------------------------------------------------- def __eq__(self, other): """Check equality on all immutable fields (i.e. not "meta"). Parameters ---------- other : `~astropy.cosmology.Cosmology` subclass instance The object in which to compare. Returns ------- bool True if all immutable fields are equal, False otherwise. """ if not isinstance(other, Cosmology): return False sias, oias = self._init_arguments, other._init_arguments # check if the cosmologies have identical signatures. # this protects against one cosmology having a superset of input # parameters to another cosmology. if (sias.keys() ^ oias.keys()) - {'meta'}: return False # are all the non-excluded immutable arguments equal? return all( (np.all(oias[k] == v) for k, v in sias.items() if k != "meta"))