Units¶
In Synthesizer all quantities a user interacts with (that are not dimensionless) have units associated with them. We implement this unit system using the unyt
package.
Synthesizer objects and methods should always be provided with quantites and associated units. This can be easily achieved with the unyt package.
[1]:
from unyt import Mpc
# Define a variable with units
x = 1 * Mpc
print(x)
print("x is now a unyt_quantity: type(x)=", type(x))
1 Mpc
x is now a unyt_quantity: type(x)= <class 'unyt.array.unyt_quantity'>
All unit functionality in synthesizer is contained in the units
module. This module contains an importable object containing the default units of all attributes throughout synthesizer.
[2]:
from synthesizer.units import default_units
print(default_units)
+-------------------------------------------------+
| DEFAULT UNITS |
+-------------------------------+-----------------+
| Category | Unit |
+-------------------------------+-----------------+
| spatial | Mpc |
+-------------------------------+-----------------+
| mass | Msun |
+-------------------------------+-----------------+
| time | yr |
+-------------------------------+-----------------+
| velocity | km/s |
+-------------------------------+-----------------+
| temperature | K |
+-------------------------------+-----------------+
| angle | degree |
+-------------------------------+-----------------+
| wavelength | Å |
+-------------------------------+-----------------+
| frequency | Hz |
+-------------------------------+-----------------+
| luminosity | erg/s |
+-------------------------------+-----------------+
| luminosity_density_frequency | erg/(Hz*s) |
+-------------------------------+-----------------+
| luminosity_density_wavelength | erg/(s*Å) |
+-------------------------------+-----------------+
| flux | erg/(cm**2*s) |
+-------------------------------+-----------------+
| flux_density_frequency | nJy |
+-------------------------------+-----------------+
| flux_density_wavelength | erg/(cm**2*s*Å) |
+-------------------------------+-----------------+
| mass_rate | Msun/yr |
+-------------------------------+-----------------+
The Units
object¶
The unit system is defined by the Units
object. This object contains a collection of attributes defining the units associated to each “category” of quantity throughout Synthesizer.
Importantly, Units
is a Singleton
object. This means there can only ever be one instance of Units
; if a second is instantiated then the first is returned. This ensures that the unit system remains consistent when running Synthesizer.
[3]:
from synthesizer.units import Units
# Define multiple Units instances
units1 = Units()
units2 = Units()
print("Both units instances are the same object:", units1 is units2)
Both units instances are the same object: True
You can take a look at the unit system by printing the instance of Units
.
[4]:
print(units1)
+-------------------------------------------------+
| UNIT SYSTEM |
+-------------------------------+-----------------+
| Category | Unit |
+-------------------------------+-----------------+
| spatial | Mpc |
+-------------------------------+-----------------+
| mass | Msun |
+-------------------------------+-----------------+
| time | yr |
+-------------------------------+-----------------+
| velocity | km/s |
+-------------------------------+-----------------+
| temperature | K |
+-------------------------------+-----------------+
| angle | degree |
+-------------------------------+-----------------+
| wavelength | Å |
+-------------------------------+-----------------+
| frequency | Hz |
+-------------------------------+-----------------+
| luminosity | erg/s |
+-------------------------------+-----------------+
| luminosity_density_frequency | erg/(Hz*s) |
+-------------------------------+-----------------+
| luminosity_density_wavelength | erg/(s*Å) |
+-------------------------------+-----------------+
| flux | erg/(cm**2*s) |
+-------------------------------+-----------------+
| flux_density_frequency | nJy |
+-------------------------------+-----------------+
| flux_density_wavelength | erg/(cm**2*s*Å) |
+-------------------------------+-----------------+
| mass_rate | Msun/yr |
+-------------------------------+-----------------+
Modifying the default unit system¶
If the default unit system works for your needs then you don’t need to do anything. You will never interact with the Units
object and all quantites will have the default units associated to them automatically. However, if you need to change one or more of the units used you can import Units
and instantiate it with a dictionary of the modified quantities.
This dictionary of modified quantities can either modify an existing category or it can defining a unit for a specific attribute. We demonstrate this below by modifying the default unit system to use kpc
for the "spatial"
category, but also override this to use Mpc
for coordinates and Myr
for ages specifically.
[5]:
# Ensure warnings are printed
import warnings
from unyt import Mpc, Msun, Myr, kpc
warnings.simplefilter("always")
# Make the dictionary containing the units we want to change
new_units = {
"spatial": kpc,
"coordinates": Mpc,
"ages": Myr,
}
# Set up the modified unit system
units = Units(new_units)
print()
print(units)
+-------------------------------------------------+
| UNIT SYSTEM |
+-------------------------------+-----------------+
| Category | Unit |
+-------------------------------+-----------------+
| spatial | Mpc |
+-------------------------------+-----------------+
| mass | Msun |
+-------------------------------+-----------------+
| time | yr |
+-------------------------------+-----------------+
| velocity | km/s |
+-------------------------------+-----------------+
| temperature | K |
+-------------------------------+-----------------+
| angle | degree |
+-------------------------------+-----------------+
| wavelength | Å |
+-------------------------------+-----------------+
| frequency | Hz |
+-------------------------------+-----------------+
| luminosity | erg/s |
+-------------------------------+-----------------+
| luminosity_density_frequency | erg/(Hz*s) |
+-------------------------------+-----------------+
| luminosity_density_wavelength | erg/(s*Å) |
+-------------------------------+-----------------+
| flux | erg/(cm**2*s) |
+-------------------------------+-----------------+
| flux_density_frequency | nJy |
+-------------------------------+-----------------+
| flux_density_wavelength | erg/(cm**2*s*Å) |
+-------------------------------+-----------------+
| mass_rate | Msun/yr |
+-------------------------------+-----------------+
/tmp/ipykernel_7129/3361722145.py:16: RuntimeWarning: Units are already set. Any modified units will not take effect. Units should be configured before running anything else... but you could (and shouldn't) force it: Units(new_units_dict, force=True).
units = Units(new_units)
You’ll notice that something has gone wrong here… but recall that the unit system will return the original if one exists, so actually this should be completely expected.
This issue highlights the need to set up Units
before doing anything else. If any computations have been done the Units
instance will exist and will not be modifiable after the fact. However, should you fall in this trap the code will warn you as above - no hidden gotchas here!
Now, lets go against the advice above, and use the highly inadvisable force argument to get a new Unit system. But please note, in a real use case, forcing a modified unit system WILL NOT convert existing quantities to the new unit system.
[6]:
# Set up the modified unit system
units = Units(new_units, force=True)
print()
print(units)
Redefining unit system:
spatial: kpc
coordinates: Mpc
ages: Myr
+-------------------------------------------------+
| UNIT SYSTEM |
+-------------------------------+-----------------+
| Category | Unit |
+-------------------------------+-----------------+
| spatial | kpc |
+-------------------------------+-----------------+
| mass | Msun |
+-------------------------------+-----------------+
| time | yr |
+-------------------------------+-----------------+
| velocity | km/s |
+-------------------------------+-----------------+
| temperature | K |
+-------------------------------+-----------------+
| angle | degree |
+-------------------------------+-----------------+
| wavelength | Å |
+-------------------------------+-----------------+
| frequency | Hz |
+-------------------------------+-----------------+
| luminosity | erg/s |
+-------------------------------+-----------------+
| luminosity_density_frequency | erg/(Hz*s) |
+-------------------------------+-----------------+
| luminosity_density_wavelength | erg/(s*Å) |
+-------------------------------+-----------------+
| flux | erg/(cm**2*s) |
+-------------------------------+-----------------+
| flux_density_frequency | nJy |
+-------------------------------+-----------------+
| flux_density_wavelength | erg/(cm**2*s*Å) |
+-------------------------------+-----------------+
| mass_rate | Msun/yr |
+-------------------------------+-----------------+
| coordinates | Mpc |
+-------------------------------+-----------------+
| ages | Myr |
+-------------------------------+-----------------+
Permenantly modify the default unit system¶
If you want to permenantly modify the default unit system you can do so by first modifying the Units
object and then calling the overwrite_defaults_yaml
method. This will write the modified unit system to the default units file.
Note, you can also explicitly edit the default_units.yml
file within the source code.
[7]:
units.overwrite_defaults_yaml()
Original unit system has been preserved at /home/runner/work/synthesizer/synthesizer/src/synthesizer/original_units.yml.
Default unit system has been updated at /home/runner/work/synthesizer/synthesizer/src/synthesizer/default_units.yml.
When the above function is called we also write out the original default units system if it hasn’t already been written out. This ensures the unit redefinition is reversible. To revert to the original unit system you can call the reset_defaults_yaml
method.
[8]:
units.reset_defaults_yaml()
print(units)
Default unit system has been reset to /home/runner/work/synthesizer/synthesizer/src/synthesizer/default_units.yml.
+-------------------------------------------------+
| UNIT SYSTEM |
+-------------------------------+-----------------+
| Category | Unit |
+-------------------------------+-----------------+
| spatial | Mpc |
+-------------------------------+-----------------+
| mass | Msun |
+-------------------------------+-----------------+
| time | yr |
+-------------------------------+-----------------+
| velocity | km/s |
+-------------------------------+-----------------+
| temperature | K |
+-------------------------------+-----------------+
| angle | degree |
+-------------------------------+-----------------+
| wavelength | Å |
+-------------------------------+-----------------+
| frequency | Hz |
+-------------------------------+-----------------+
| luminosity | erg/s |
+-------------------------------+-----------------+
| luminosity_density_frequency | erg/(Hz*s) |
+-------------------------------+-----------------+
| luminosity_density_wavelength | erg/(s*Å) |
+-------------------------------+-----------------+
| flux | erg/(cm**2*s) |
+-------------------------------+-----------------+
| flux_density_frequency | nJy |
+-------------------------------+-----------------+
| flux_density_wavelength | erg/(cm**2*s*Å) |
+-------------------------------+-----------------+
| mass_rate | Msun/yr |
+-------------------------------+-----------------+
Working with Quantity
objects¶
There is no need to work with the Units
object itself beyond initially defining a modified unit system. Beyond this, all unit operations are handled “behind the scenes”. This hidden functionality is enabled by the Quantity
object.
All attributes on Synthesizer objects which carry units are in fact Quantity
objects. Quantity
objects carry a the unit of the attribute (extracted from the global unit system), and extract the appropriate units depending on the name of the variable storing the Quantity
. As such, a user will never instantiate a quantity themselves, but their usage is important.
One simple thing to keep in mind is how to return the value with or without units. This is achieved by the application or omission of a leading underscore to a variable name.
Lets create an Sed
object, which has a wavelength array stored under lam
.
[9]:
import numpy as np
from unyt import Hz, angstrom, erg, s
from synthesizer.emissions import Sed
# Make an sed with arbitrary arguments
sed = Sed(
lam=np.linspace(10, 1000, 10) * angstrom, lnu=np.ones(10) * erg / s / Hz
)
We can access this attribute with units as you would expect to access any attribute.
[10]:
print(sed.lam)
[ 10. 120. 230. 340. 450. 560. 670. 780. 890. 1000.] Å
Or we can append a leading underscore and return it without units.
[11]:
print(sed._lam)
[ 10. 120. 230. 340. 450. 560. 670. 780. 890. 1000.]
In the case of compound units this is somewhat less elegant. Lets demonstrate with a Stars
object.
[12]:
from synthesizer.particle.stars import Stars
# Create a dummy Stars object
stars = Stars(
initial_masses=np.random.rand(10) * Msun,
ages=np.ones(10) * Myr,
metallicities=np.ones(10),
)
If we print the initial_masses
with units we get the compound version in kg
.
[13]:
print(stars.initial_masses)
[0.92451456 0.97376841 0.53352426 0.68442758 0.39005032 0.1581944
0.55953829 0.28040954 0.47422623 0.16337153] Msun
However, if we extract the values alone we get the values we expect in \(M_\odot\).
[14]:
print(stars._initial_masses)
[0.92451456 0.97376841 0.53352426 0.68442758 0.39005032 0.1581944
0.55953829 0.28040954 0.47422623 0.16337153]
Its worth keeping this in mind whenever extracting masses from synthesizer objects.
Automatic unit conversion¶
Finally, let’s utilise some automatic unit conversion. If we input a mixture of properties to a synthesizer object, all with different units to the global unit system, we don’t have to convert them all before inputting them. As long as we pass them to synthesizer with unyt units attached, the conversion will be handled automatically. Here we use a Stars
object again to demonstrate.
[15]:
from unyt import Mpc, g, m
# Create a dummy Stars object
stars = Stars(
initial_masses=np.random.rand(10) * 10**34.0 * g,
ages=np.ones(10) * Myr,
metallicities=np.ones(10),
coordinates=np.random.rand(10, 3) * Mpc,
smoothing_lengths=np.random.rand(10) * 10**22.0 * m,
)
print(
"stars.initial_masses[0]=",
stars.initial_masses[0],
"=",
stars._initial_masses[0],
"Msun",
)
print("stars.ages[0]=", stars.ages[0])
print("stars.coordinates[0]=", stars.coordinates[0])
print("stars.smoothing_lengths[0]=", stars.smoothing_lengths[0])
stars.initial_masses[0]= 0.7473979946192165 Msun = 0.7473979946192165 Msun
stars.ages[0]= 1000000.0 yr
stars.coordinates[0]= [0.20215989 0.25979976 0.96248465] Mpc
stars.smoothing_lengths[0]= 0.2081158599685886 Mpc