Wrapping an Existing Library#
Many cosmology libraries already exist, such as astropy.cosmology,
CLASS, and CAMB, and all have different inerfaces that are not
compatible with each other. The Cosmology API is designed to be a common
interface and we can easily wrap existing codes to make them compatible with the
Cosmology API. Many of the common cosmology libraries are already wrapped and
available in the cosmology.compat module.
If there is an unsopported library that you would like to wrap, the Cosmology
API is designed to be as simple as possible. Essentially, you need to create an
object that implements the cosmology.api.CosmologyWrapper interface, as
well any other pieces (e.g. cosmology.api.DistanceMeasures) that you
want to support and map the methods to the appropriate functions on the
cosmology object / in the library.
Let’s work through an example. Suppose you have a library that provides a
Cosmology class that only has the cosmological parameters and the rest
of the cosmological calculations are done by functions in the library. The
library also has some constants, such as the speed of light, that are used in
calculations. The library looks something like this:
example_library/
__init__.py
some_constants.py
- speed_of_light
other_constants.py
- gravitational constant
cosmology_class.py
- ExampleCosmology
functions.py
- comoving_distance_z1z2()
- hubble_parameter()
- omega_matter()
- omega_photon()
- omega_lambda()
The cosmology class looks something like this:
from dataclasses import dataclass
@dataclass
class ExampleCosmology:
H0: float
Om0: float
Ogamma0: float
Ode0: float
Now let’s wrap this library. First, we need to create a wrapping of the
top-level namespace so that it implements the
cosmology.api.CosmologyNamespace interface. This will also require
creating a namespace for the constants.
# wrapper/__init__.py
from . import constants
...
# wrapper/constants.py
from example_library import some_constants, other_constants
c = some_constants.speed_of_light
G = other_constants.gravitational_constant
Next we need to create a wrapper class that implements the
CosmologyWrapper interface. Note that by the magic of
protocols we do not need to subclass anything to be considered a subclass of
CosmologyWrapper, we just need to implement the methods.
import wrapper
@dataclass(frozen=True)
class BaseExampleLibraryWrapper:
cosmo: ExampleCosmology
# ExampleCosmology does not have a name, but we can support one.
# If we don't want to, the name can be a property that always returns None.
name: str | None = None
@property
def __cosmology_namespace__(self):
return wrapper
@property
def constants(self):
return self.__cosmology_namespace__.constants
To this base wrapper, we can add any other pieces that we want to support, such
as HubbleParameter,
MatterComponent,
PhotonComponent,
DarkEnergyComponent, and
ComovingDistanceMeasures. Note that the Cosmology API is
built on the Array API and all outputs must be some conformant array type. A
common choice is numpy.ndarray, but any array type that implements the
Array API will work. For this example we will add support for
HubbleParameter and
ComovingDistanceMeasures.
from typing import Any, Union
from typing_extensions import TypeAlias
import numpy as np
Array: TypeAlias = np.ndarray[Any, np.floating[Any]]
InputT: TypeAlias = Union[Array, float]
class ExampleLibraryWrapper(BaseExampleLibraryWrapper):
# - HubbleParameter -----
@property
def H0(self) -> Array:
return np.array(self.cosmo.H0)
def H(self, z: InputT) -> Array:
return example_library.hubble_parameter(self.cosmo, z)
@property
def hubble_distance(self) -> Array:
return np.array(self.constants.c / self.H0 * converstion_to_Mpc)
@property
def hubble_time(self) -> Array:
return np.array(1 / self.H0 * converstion_to_Gyr)
# - ComovingDistanceMeasures -----
def comoving_distance(self, z1: InputT, z2: InputT | None = None) -> Array:
z1, z2 = (z1, z2) if z2 is not None else (0, z1)
return example_library.comoving_distance_z1z2(self.cosmo, z1, z2)
def transverse_comoving_distance(
self, z1: InputT, z2: InputT | None = None
) -> Array:
... # up to you to implement this
def comoving_volume(self, z1: InputT, z2: InputT | None = None) -> Array:
... # up to you to implement this
def differential_comoving_volume(
self, z1: InputT, z2: InputT | None = None
) -> Array:
... # up to you to implement this
Great! Now we have a wrapper that implements the base Cosmology API and supports
a number of additional components – though not yet all the ones that are in
example_library.
Does this implement the full StandardCosmology
interface? No! But instances of this class can be used anywhere that only
requires the HubbleParameter or
ComovingDistanceMeasures methods. If functions are well
written to only require the cosmology attributes and methods that they need,
then this wrapper can be used in those functions.
from cosmology.api import HasH0, StandardCosmology
def littleh_too_broad(cosmo: StandardCosmology): # Not guaranteed to work!
return cosmo.H0 / 100
def littleh(cosmo: HasH0): # Guaranteed to work!
return cosmo.H0 / 100
To keep things simple the above ExampleLibraryWrapper only implemented the
HubbleParameter and
ComovingDistanceMeasures methods. However, the
example_library also has a number of other components that we can add to our
wrapper: MatterComponent,
PhotonComponent, and
DarkEnergyComponent. We could go back and add these to
the ExampleLibraryWrapper class, or we can create a new wrapper that
inherits from ExampleLibraryWrapper and adds the additional components,
whatever is most convenient for your use case. For this example we will create a
“mixin” class for MatterComponent that may be used in
either scenario.
class MatterMixin:
cosmo: ExampleCosmology
@property
def Omega_m0(self) -> Array:
return np.array(self.cosmo.Om0)
def Omega_m(self, z: InputT) -> Array:
return example_library.omega_matter(self.cosmo, z)
The point is that ExampleLibraryWrapper can be extended to support as much
or as little of the Cosmology API as is needed. Though it would be nice if
functions were written to only require the cosmology attributes and methods that
they need, it is expected that many functions are too broadly typed e.g.
littleh_too_broad above. Consequently, if the wrapper can support the full
StandardCosmology it is recommended to do so. However,
if only a subset of the API is possible, then it is better to implement that
subset than to not.