Black Hole Spectra¶
Black hole spectra can be generated by combining a BlackHoles
(for particle, BlackHole
for parametric) object with an EmissionModel
, translating the physical properties of the blackhole(s) (e.g. mass
, accretion_rate
, etc.) to a spectral energy distribution.
These models are described in detail in the emission model docs. Here, we’ll use an instance of a UnifiedAGN
model for demonstration purposes.
The following sections demonstrate the generation of combined spectra (which is the same for both parametric and particle BlackHoles
) and per-particle spectra.
[1]:
import numpy as np
from unyt import K, Mpc, Msun, deg, yr
from synthesizer import Grid
from synthesizer.emission_models import (
AttenuatedEmission,
DustEmission,
UnifiedAGN,
)
from synthesizer.emission_models.attenuation import PowerLaw
from synthesizer.emission_models.dust.emission import Greybody
from synthesizer.parametric import BlackHole
# Get the NLR and BLR grids
nlr_grid = Grid("test_grid_agn-nlr")
blr_grid = Grid("test_grid_agn-blr")
uniagn = UnifiedAGN(
nlr_grid,
blr_grid,
covering_fraction_nlr=0.1,
covering_fraction_blr=0.1,
torus_emission_model=Greybody(1000 * K, 1.5),
ionisation_parameter=0.1,
hydrogen_density=1e5,
)
blackhole = BlackHole(
mass=1e8 * Msun,
inclination=60 * deg,
accretion_rate=1 * Msun / yr,
metallicity=0.01,
)
Integrated spectra¶
To generate integrated spectra we simply call the component’s get_spectra
method. This method will populate the component’s spectra
attribute with a dictionary containing Sed objects for each spectra in the EmissionModel
. It will also return the spectra at the root of the EmissionModel
.
[2]:
# Get the spectra using a unified agn model (instantiated elsewhere)
spectra = blackhole.get_spectra(uniagn)
fig, ax = blackhole.plot_spectra(
show=True, ylimits=(10**27.5, 10**34.0), figsize=(6, 4)
)
/opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/unyt/array.py:1832: RuntimeWarning: overflow encountered in exp
out_arr = func(np.asarray(inp), out=out_func, **kwargs)
/opt/hostedtoolcache/Python/3.10.18/x64/lib/python3.10/site-packages/unyt/array.py:1972: RuntimeWarning: overflow encountered in multiply
out_arr = func(

Including dust attenuation¶
We can also generate spectra including attenuation and emission from diffuse dust along the line of sight to the black hole. We do this by combining the UnifiedAGN
model with models defining these attenuation and emission contributions.
First we define the emission models (for more details see the attenuation documentation).
[3]:
tau_v = 0.5
dust_curve = PowerLaw(slope=-1.0)
dust_emission_model = Greybody(30 * K, 1.2)
# Define an emission model to attenuate the intrinsic AGN emission
att_model = AttenuatedEmission(
dust_curve=dust_curve,
apply_to=uniagn,
tau_v=tau_v,
emitter="blackhole",
)
# And now include the dust emission
dust_model = DustEmission(
dust_emission_model=dust_emission_model,
dust_lum_intrinsic=uniagn,
dust_lum_attenuated=att_model,
emitter="blackhole",
)
We then follow the same process of calling get_spectra
with the new model. The plot here shows luminosity rather than spectral energy density.
[4]:
spectra = blackhole.get_spectra(dust_model)
fig, ax = blackhole.plot_spectra(quantity_to_plot="luminosity", figsize=(6, 4))

The spectra returned by get_spectra
is the “dust_emission” spectra at the root of the emission model.
[5]:
print(spectra)
+-------------------------------------------------------------------------------------------------+
| SED |
+---------------------------+---------------------------------------------------------------------+
| Attribute | Value |
+---------------------------+---------------------------------------------------------------------+
| redshift | 0 |
+---------------------------+---------------------------------------------------------------------+
| ndim | 1 |
+---------------------------+---------------------------------------------------------------------+
| nlam | 9244 |
+---------------------------+---------------------------------------------------------------------+
| shape | (9244,) |
+---------------------------+---------------------------------------------------------------------+
| lam (9244,) | 1.30e-04 Å -> 2.99e+11 Å (Mean: 9.73e+09 Å) |
+---------------------------+---------------------------------------------------------------------+
| nu (9244,) | 1.00e+07 Hz -> 2.31e+22 Hz (Mean: 8.51e+19 Hz) |
+---------------------------+---------------------------------------------------------------------+
| lnu (9244,) | 0.00e+00 erg/(Hz*s) -> 1.35e+33 erg/(Hz*s) (Mean: 5.76e+31 erg) |
+---------------------------+---------------------------------------------------------------------+
| bolometric_luminosity | 4.496423245270723e+45 erg/s |
+---------------------------+---------------------------------------------------------------------+
| energy (9244,) | 4.14e-08 eV -> 9.56e+07 eV (Mean: 3.52e+05 eV) |
+---------------------------+---------------------------------------------------------------------+
| frequency (9244,) | 1.00e+07 Hz -> 2.31e+22 Hz (Mean: 8.51e+19 Hz) |
+---------------------------+---------------------------------------------------------------------+
| llam (9244,) | 0.00e+00 erg/(s*Å) -> 4.64e+39 erg/(s*Å) (Mean: 1.56e+38 erg/(s*Å)) |
+---------------------------+---------------------------------------------------------------------+
| luminosity (9244,) | 0.00e+00 erg/s -> 3.92e+45 erg/s (Mean: 1.46e+44 erg/s) |
+---------------------------+---------------------------------------------------------------------+
| luminosity_lambda (9244,) | 0.00e+00 erg/(s*Å) -> 4.64e+39 erg/(s*Å) (Mean: 1.56e+38 erg/(s*Å)) |
+---------------------------+---------------------------------------------------------------------+
| luminosity_nu (9244,) | 0.00e+00 erg/(Hz*s) -> 1.35e+33 erg/(Hz*s) (Mean: 5.76e+31 erg) |
+---------------------------+---------------------------------------------------------------------+
| wavelength (9244,) | 1.30e-04 Å -> 2.99e+11 Å (Mean: 9.73e+09 Å) |
+---------------------------+---------------------------------------------------------------------+
However, all the spectra are stored within a dictionary under the spectra
attribute.
[6]:
print(blackhole.spectra)
{'disc_incident': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6b250>, 'full_disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfa170>, 'full_disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfa200>, 'full_reprocessed_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfae30>, 'full_reprocessed_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfa080>, 'disc_incident_isotropic': <synthesizer.emissions.sed.Sed object at 0x7f2f9adf9db0>, 'disc_escaped': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfae00>, 'disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adf9f90>, 'disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfa950>, 'disc_transmitted': <synthesizer.emissions.sed.Sed object at 0x7f2f9cf6f220>, 'disc': <synthesizer.emissions.sed.Sed object at 0x7f2fd8da9630>, 'blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6a4d0>, 'nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6ad70>, 'torus': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfac50>, 'intrinsic': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6b010>, 'attenuated': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6a7a0>, 'dust_emission': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfab00>}
Particle spectra¶
To demonstrate the particle spectra functionality we first generate some mock particle black hole data, and initialise a BlackHoles
object.
[7]:
from synthesizer.particle import BlackHoles
# Make fake properties
n = 4
masses = 10 ** np.random.uniform(low=7, high=9, size=n) * Msun
coordinates = np.random.normal(0, 1.5, (n, 3)) * Mpc
accretion_rates = 10 ** np.random.uniform(low=-2, high=1, size=n) * Msun / yr
metallicities = np.full(n, 0.01)
# And get the black holes object
blackholes = BlackHoles(
masses=masses,
coordinates=coordinates,
accretion_rates=accretion_rates,
metallicities=metallicities,
)
To generate a spectra for each black hole (per particle) we use the same emission model, but we need to tell the model to produce a spectrum for each particle. This is done by setting the per_particle
flag to True
on the model.
[8]:
dust_model.set_per_particle(True)
With that done we just call the same get_spectra
method on the component, and the particle spectra will be stored in the particle_spectra
attribute of the component.
[9]:
spectra = blackholes.get_spectra(dust_model, verbose=True)
Again, the returned spectra is the “dust_emission” spectra from the root of the model.
[10]:
print(spectra)
+---------------------------------------------------------------------------------------------------+
| SED |
+-----------------------------+---------------------------------------------------------------------+
| Attribute | Value |
+-----------------------------+---------------------------------------------------------------------+
| redshift | 0 |
+-----------------------------+---------------------------------------------------------------------+
| ndim | 2 |
+-----------------------------+---------------------------------------------------------------------+
| nlam | 9244 |
+-----------------------------+---------------------------------------------------------------------+
| shape | (4, 9244) |
+-----------------------------+---------------------------------------------------------------------+
| lam (9244,) | 1.30e-04 Å -> 2.99e+11 Å (Mean: 9.73e+09 Å) |
+-----------------------------+---------------------------------------------------------------------+
| nu (9244,) | 1.00e+07 Hz -> 2.31e+22 Hz (Mean: 8.51e+19 Hz) |
+-----------------------------+---------------------------------------------------------------------+
| lnu (4, 9244) | 0.00e+00 erg/(Hz*s) -> 5.37e+32 erg/(Hz*s) (Mean: 9.62e+30 erg) |
+-----------------------------+---------------------------------------------------------------------+
| bolometric_luminosity (4,) | 1.19e+44 erg/s -> 1.79e+45 erg/s (Mean: 7.51e+44 erg/s) |
+-----------------------------+---------------------------------------------------------------------+
| energy (9244,) | 4.14e-08 eV -> 9.56e+07 eV (Mean: 3.52e+05 eV) |
+-----------------------------+---------------------------------------------------------------------+
| frequency (9244,) | 1.00e+07 Hz -> 2.31e+22 Hz (Mean: 8.51e+19 Hz) |
+-----------------------------+---------------------------------------------------------------------+
| llam (4, 9244) | 0.00e+00 erg/(s*Å) -> 1.84e+39 erg/(s*Å) (Mean: 2.60e+37 erg/(s*Å)) |
+-----------------------------+---------------------------------------------------------------------+
| luminosity (4, 9244) | 0.00e+00 erg/s -> 1.56e+45 erg/s (Mean: 2.44e+43 erg/s) |
+-----------------------------+---------------------------------------------------------------------+
| luminosity_lambda (4, 9244) | 0.00e+00 erg/(s*Å) -> 1.84e+39 erg/(s*Å) (Mean: 2.60e+37 erg/(s*Å)) |
+-----------------------------+---------------------------------------------------------------------+
| luminosity_nu (4, 9244) | 0.00e+00 erg/(Hz*s) -> 5.37e+32 erg/(Hz*s) (Mean: 9.62e+30 erg) |
+-----------------------------+---------------------------------------------------------------------+
| wavelength (9244,) | 1.30e-04 Å -> 2.99e+11 Å (Mean: 9.73e+09 Å) |
+-----------------------------+---------------------------------------------------------------------+
While the spectra produced by get_particle_spectra
are stored in a dictionary under the particle_spectra
attribute.
[11]:
print(blackholes.particle_spectra)
{'disc_incident': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad687c0>, 'full_disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad694e0>, 'full_disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9acd8670>, 'full_reprocessed_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9acd83d0>, 'full_reprocessed_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9acd9690>, 'disc_incident_isotropic': <synthesizer.emissions.sed.Sed object at 0x7f2f9ac71060>, 'disc_escaped': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69330>, 'disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68b50>, 'disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad682b0>, 'disc_transmitted': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68fd0>, 'disc': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68370>, 'blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68df0>, 'nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69510>, 'torus': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68ee0>, 'intrinsic': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69f60>, 'attenuated': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68a60>, 'dust_emission': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6b070>}
Integrating spectra¶
The integrated spectra are automatically produced alongside per particle spectra. However, if we wanted to explictly get the integrated spectra from the particle spectra we just generated (for instance if we had made some modification after generation), we can call the integrate_particle_spectra
method. This method will sum the individual spectra and populate the spectra
dictionary.
Note, we can also integrate individual spectra using the Sed.sum()
method.
[12]:
print(blackholes.spectra)
blackholes.integrate_particle_spectra()
print(blackholes.spectra)
fig, ax = blackholes.plot_spectra(show=True, figsize=(6, 4))
{'disc_incident': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68550>, 'full_disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68bb0>, 'full_disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ac73190>, 'full_reprocessed_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ac71960>, 'full_reprocessed_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69ea0>, 'disc_incident_isotropic': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68d90>, 'disc_escaped': <synthesizer.emissions.sed.Sed object at 0x7f2f9ac71990>, 'disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68b80>, 'disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69180>, 'disc_transmitted': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69420>, 'disc': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69060>, 'blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68400>, 'nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68520>, 'torus': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad690c0>, 'intrinsic': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6bdf0>, 'attenuated': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6bfa0>, 'dust_emission': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6b130>}
{'disc_incident': <synthesizer.emissions.sed.Sed object at 0x7f2f9acd8940>, 'full_disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69090>, 'full_disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68bb0>, 'full_reprocessed_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adfae60>, 'full_reprocessed_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9adf98a0>, 'disc_incident_isotropic': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69ea0>, 'disc_escaped': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68d90>, 'disc_transmitted_blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68640>, 'disc_transmitted_nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68b80>, 'disc_transmitted': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69180>, 'disc': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69420>, 'blr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad69060>, 'nlr': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68400>, 'torus': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad68520>, 'intrinsic': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad690c0>, 'attenuated': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6bdf0>, 'dust_emission': <synthesizer.emissions.sed.Sed object at 0x7f2f9ad6bfa0>}

Modifying EmissionModel
parameters with get_spectra
¶
As well as modifying a model explicitly, it’s also possible to override the properties of a model at the point get_spectra
is called. These modifications will not be remembered by the model afterwards. As it stands, this form of modification is supported for the dust_curve
, tau_v
, covering_fraction
and masks
.
Here we’ll demonstrate this by overriding the covering fractions to generate spectra for a range of covering_fraction
values. This can either be done by passing a single number which will overide all covering fractions on every model (probably undesirable behaviour since this will set NLR and BLR covering fractions to be equal).
[13]:
# Since we now want integrated spectra lets remove the per particle flag
dust_model.set_per_particle(False)
blackhole.clear_all_spectra()
spectra = {}
for fesc in [0.1, 0.5, 0.9]:
blackhole.get_spectra(dust_model, covering_fraction=fesc)
spectra[r"$f " f"=$ {fesc}"] = blackhole.spectra["intrinsic"]
Or (more desirably) we can pass a dictionary mapping model labels to fesc
values to target specific models. Notice that we have invoked the clear_all_spectra
method to reset the spectra dictionary, we can also clear all emissions (including spectra, lines, and photometry if they are present) with the clear_all_emissions
method.
[14]:
blackhole.clear_all_emissions()
spectra = {}
for nlr_fesc in [0.1, 0.5, 0.9]:
for blr_fesc in [0.1, 0.5, 0.9]:
if nlr_fesc + blr_fesc > 1:
continue
blackhole.get_spectra(
dust_model,
covering_fraction={
"disc_transmitted_nlr": nlr_fesc,
"disc_transmitted_blr": blr_fesc,
},
)
spectra[
r"$f_\mathrm{nlr} "
f"=$ {nlr_fesc}, "
r"$f_\mathrm{blr} "
f"=$ {blr_fesc},"
] = blackhole.spectra["intrinsic"]
To see the variation above we can pass the dictionary we populated with the varied spectra to the plot_spectra
function (where the dictionary keys will be used as labels).
[15]:
from synthesizer.emissions import plot_spectra
plot_spectra(
spectra,
figsize=(8, 4),
ylimits=(10**29.0, 10**34.0),
xlimits=(9 * 10**2, 10**4),
show=True,
)

[15]:
(<Figure size 800x400 with 1 Axes>,
<Axes: xlabel='$\\lambda/[\\mathrm{\\AA}]$', ylabel='$L_{\\nu}/[\\mathrm{\\rm{erg} \\ / \\ \\rm{Hz \\cdot \\rm{s}}}]$'>)