(example)=
# Example
## Effect of ionization parameters
The Python interface is particularly powerful for running a grid of models on a cluster. The example `run.sh` script, shown in Listing follows, is configured to demonstrate this by looping over 16 different values of the ionization parameter, `zeta`.
Click to expand run.sh script
```bash
PYTHON_SCRIPT="modelrun.py" # <-- Make sure this is your main Python script name
DIY="dxi5000"
# --- Execution & Model Flags ---
exec=1 # (0: Generate files only, 1: Submit with sbatch, 2: Run locally)
debug=0 # Debug switch
model=0 # (0: Reflection, 1: Pure scattering)
ity=2 # Type of angle bins and integral method
# --- Computational Grids ---
nmaxmain=15 # Number of maximum main iteration steps
nmaxrte=100 # Number of maximum radiative transfer iteration steps
ndrt=200 # Number of depth grids (tau)
nfrt=5000 # Number of energy grids [Must match kernel file]
nmurt=10 # Number of angle grids (mu)
ppemin=0.1 # Minimum energy of the grid [eV]
ppemax=9e5 # Before maximum energy of the grid [eV]
ppemax2=1e6 # Maximum energy of the grid [eV]
taumin=1e-4 # Minimum Thomson optical depth of the grid
taumaxrt=10.0 # Maximum Thomson optical depth of the grid
mumin=0.05 # Minimum mu value for the angular grid (> 0)
mumax=0.95 # Maximum mu value for the angular grid (< 1)
# --- Temperature & Equilibrium ---
it_temp=99 # Maximum iteration steps for thermal equilibrium
temp_gas=1e8 # Initial gas temperature
temp_gas_unit='k' # Unit of the initial temperature ('k' or 'ev')
# --- Incident Spectrum Parameters ---
inci_type="file" # Type of corona illumination ('powerlaw', 'file', 'cutoff')
gamma=1e3 # Photon index or Blackbody temperature in eV
hcut=300e3 # High energy cut-off for cut-off power law illumination [eV]
inmu=0.7 # Cosine of the incident illumination angle
inci_file="incident/nthcomp_g2.4_t60.txt" # Path to incident spectrum file
# --- Bottom Illumination (Disk) ---
sbot=0 # Switch for bottom illumination (0: off, 1: on)
ktbb=350.0 # Temperature of the bottom blackbody source [eV]
fx_frac=1e-10 # Flux ratio of top illumination to total illumination
# --- Atmosphere Properties ---
zeta=3.0 # Log10 of ionization parameter (xi)
nh=1e15 # Hydrogen number density [cm^-3]
fe=1.0 # Iron abundance relative to Hydrogen
# --- Paths ---
kernelpath="kernel_exact5000" # Path to the Compton scattering redistribution file
echo "----------------------------------------"
echo "Starting model run with DIY tag: ${DIY}"
echo "----------------------------------------"
# Example loop: You can loop over any parameter.
# Here, we loop over the incident angle 'inmu'.
for zeta in 1.0 1.2 1.4 1.6 1.8 2.0 2.2 2.4 2.6 2.8 3.0 3.2 3.4 3.6 3.8 4.0
for inci_file in "incident/nthcomp_g2.4_t60.txt" "incident/nthcomp_g1.4_t60.txt"
do
echo "Running with nh = ${zeta}"
python ${PYTHON_SCRIPT} \
--exec ${exec} \
--debug ${debug} \
--model ${model} \
--ity ${ity} \
--nmaxmain ${nmaxmain} \
--nmaxrte ${nmaxrte} \
--ndrt ${ndrt} \
--nfrt ${nfrt} \
--nmurt ${nmurt} \
--ppemin ${ppemin} \
--ppemax ${ppemax} \
--ppemax2 ${ppemax2} \
--taumin ${taumin} \
--taumaxrt ${taumaxrt} \
--mumin ${mumin} \
--mumax ${mumax} \
--it-temp ${it_temp} \
--temp-gas ${temp_gas} \
--temp-gas-unit "${temp_gas_unit}" \
--inci-type "${inci_type}" \
--gamma ${gamma} \
--hcut ${hcut} \
--inmu ${inmu} \
--inci-file "${inci_file}" \
--sbot ${sbot} \
--ktbb ${ktbb} \
--fx-frac ${fx_frac} \
--zeta ${zeta} \
--nh ${nh} \
--fe ${fe} \
--kernelpath "${kernelpath}" \
--diy "${DIY}"
echo "----------------------------------------"
done
done
```
Click to expand plot script
```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import json
import os
from rdata import read_spec,read_spec5000
from matplotlib.colors import Normalize
from matplotlib.cm import ScalarMappable
from datetime import datetime, time
def normtopflux(zeta,nh,top,ener):
fx = 10**zeta*nh/4/np.pi
integral = np.trapz(top,ener)
normfactor = integral/fx
return top*normfactor
plt.style.use('seaborn-v0_8-ticks')
mpl.rcParams['text.usetex'] = True
mpl.rcParams['text.latex.preamble'] = r'\usepackage{mathptmx}'
mpl.rcParams['font.family'] = 'serif'
mpl.rcParams['font.size'] = 20
ERGSEV = 1.60217662e-12
# All metadata data
METADATA_DIR = 'metadata'
if not os.path.isdir(METADATA_DIR):
raise FileNotFoundError(f"no '{METADATA_DIR}' file")
datas = os.listdir(METADATA_DIR)
# get all data
plot_data_list = []
for data_filename in datas:
with open(os.path.join(METADATA_DIR, data_filename), 'r') as f:
try:
data = json.load(f)
params = data['parameters']
if params.get('diy_tag') == 'dxi5000':
try:
ener,top,bot,inte,flux = read_spec5000(params['op_spec'])
task_info = {
'zeta' : float(params['zeta']),
'ener': ener,
'eemo': flux,
'top': normtopflux(1.0, 1e15, top, ener),
'Afe': params['fe'],
'incident': params['inci_file']
}
plot_data_list.append(task_info)
except Exception as e:
continue
except (json.JSONDecodeError, KeyError, TypeError) as e:
print(f"Warning: skip {data_filename}: {e}")
continue
if not plot_data_list:
print("no 'diy_tag: dxi' ")
exit()
plot_data_list.sort(key=lambda item: item['zeta'])
zeta_values = [item['zeta'] for item in plot_data_list]
inci_files = ['incident/nthcomp_g1.4_t60.txt', 'incident/nthcomp_g2.4_t60.txt']
cmap = mpl.cm.get_cmap('viridis')
norm = Normalize(vmin=min(zeta_values), vmax=max(zeta_values))
sm = ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
fig, ax = plt.subplots(
len(inci_files), 2,
sharex='col',
sharey='row',
constrained_layout=True,
gridspec_kw={'width_ratios': [3, 1]}
)
main_ax = ax[:, 0]
zoom_ax = ax[:, 1]
fig.supylabel(r'$E J_E \quad [\mathrm{erg}^{2} \cdot \mathrm{cm}^{-2} \cdot \mathrm{s}^{-1} \cdot \mathrm{erg}^{-1}]$')
top_plotted = [False, False]
for item in plot_data_list:
color = cmap(norm(item['zeta']))
energ = item['ener']
flux = item['eemo']
top = item['top']
lw = 1.0
if item['incident'] == inci_files[0]:
main_ax[0].loglog(energ, flux, color=color, lw=lw)
if not top_plotted[0]:
axs = main_ax[0]
axs.text(0.05, 0.95, rf'$\Gamma$ = 1.4', transform=axs.transAxes,
va='top', ha='left')
main_ax[0].loglog(energ, top, color='k', lw=2,
linestyle='--', label='Incident')
top_plotted[0] = True
if item['incident'] == inci_files[1]:
main_ax[1].loglog(energ, flux, color=color, lw=lw)
if not top_plotted[1]:
axs = main_ax[1]
axs.text(0.05, 0.95, rf'$\Gamma$ = 2.4', transform=axs.transAxes,
va='top', ha='left')
main_ax[1].loglog(energ, top, color='k', lw=2,
linestyle='--', label='Incident')
top_plotted[1] = True
for i, axis in enumerate(main_ax):
axis.set_xlim(0.001, 1e3)
axis.set_ylim(10**6.5, 1e15)
if i == 0:
handles, labels = axis.get_legend_handles_labels()
if handles:
axis.legend(loc='lower right')
main_ax[-1].set_xlabel('Energy [keV]')
xticks = [1e-3, 1e-2, 1e-1, 1e0, 1e1, 1e2, 1e3]
xticklabels = [r'$10^{-3}$', r'$10^{-2}$', r'$10^{-1}$',
r'$1$', r'$10^{1}$', r'$10^{2}$', r'$10^{3}$']
main_ax[-1].set_xticks(xticks)
main_ax[-1].set_xticklabels(xticklabels)
for idx in [0, 1]:
axz = zoom_ax[idx]
zoom_flux_list = []
zoom_ener_list = []
for item in plot_data_list:
if item['incident'] == inci_files[idx]:
color = cmap(norm(item['zeta']))
ener = np.array(item['ener'])
flux = np.array(item['eemo'])
mask = (ener >= 1.0) & (ener <= 10.0)
if mask.any():
axz.loglog(ener[mask], flux[mask], lw=0.8, color=color)
zoom_flux_list.append(flux[mask])
zoom_ener_list.append(ener[mask])
axz.set_xlim(1, 10)
if zoom_flux_list:
ymin = min(np.min(f) for f in zoom_flux_list)
ymax = max(np.max(f) for f in zoom_flux_list)
axz.set_ylim(ymin * 0.8, ymax * 1.2)
axz.tick_params(axis='y', which='both', labelleft=False)
axz.text(0.05, 0.95, '1–10 keV', transform=axz.transAxes,
va='top', ha='left')
zoom_ax[-1].set_xlabel('Energy [keV]')
zoom_ax[-1].set_xticks([1e0, 1e1])
zoom_ax[-1].set_xticklabels([r'$1$', r'$10$'])
zoom_ax[-1].tick_params(axis='x', which='minor', labelbottom=False)
cbar = fig.colorbar(sm, ax=ax.ravel().tolist(), location='right', shrink=0.8)
cbar.ax.set_xlabel(r'log$\xi$')
cbar.ax.tick_params(labelsize=15)
plt.show()
```
```{figure} images/dxi.svg
:width: 100%
:align: center
:name: fig-dxi
Angle-averaged reflection spectra for various ionization parameters ($\log\xi$), distinguished by color. The `nthcomp` incident spectra are obtained from `xillvercp` by setting `refl_frac` = 0 and $kT_E = 60$keV, with $\Gamma = 1.4$ (top panel) and $\Gamma = 2.4$ (bottom panel). Other parameters are set at their default values. The incident spectrum is shown as a black dashed line for comparison.
```
## Effect of incidence angle
In `DAO`, the coronal illumination field is specified by a single incidence angle. The $I_\mathrm{inc}$ at the top boundary of the second-order radiative transfer equation is set to zero everywhere except at $\mu_{\mathrm{inc}}$.
Since the flux is defined as the first moment of the intensity:
```{math}
F_x(\tau,E) = \int_0^1 u(\tau,E,\mu) \mu d\mu
```
where $\mu = \cos\theta$. We adopt
```{math}
I = I_\mathrm{inc}\delta(\mu-\mu_\mathrm{inc})
```
The incident intensity at the top boundary can then be evaluated as:
$$
I_{\mathrm{inc}}(E) = \frac{2F_x(E)}{\mu_{\mathrm{inc}}}
$$
In DAO, we always calculate $F_x(E)$ first and then use above equation to derive $I_{\mathrm{inc}}$ for the boundary condition. This relation shows that the incidence angle $\mu_{\mathrm{inc}}$ directly affects the illumination intensity, which in turn influences the radiative transfer solution. Consequently, a smaller $\mu_{\mathrm{inc}}$ is expected to produce higher gas temperatures and a higher degree of ionization at the surface.
```{figure} images/dinci.svg
:width: 100%
:align: center
:name: fig-dxi
Top row: Angle-averaged emergent intensity (left) and the corresponding temperature profiles (right), color-coded by the incidence angle $\cos\theta_\mathrm{inc}$. Bottom row: Spectral ratios relative to the $\theta_{\mathrm{inc}} = 45^\circ$ case for all incidence angles. The incidence angles are sampled linearly in $\mu = \cos\theta_{\mathrm{inc}}$ between 0 and 1 using 30 bins. The ionization parameter is $\log\xi = 2.0$, and the incident radiation field (black dashed line) is a cut-off power-law with $\Gamma = 2.0$ and $E_\mathrm{cut} = 300~\mathrm{keV}$. The other parameters are set at their default values.
```
## Ions Fractions
We will run two simulations with different ionization parameters to compare the resulting ionic fractions. The primary physical parameters are set as follows:
* Ionization Parameter (log$\xi$): Three separate runs are performed, one with zeta
= 3.0 ,2.0 and 4.0.
* Incident Spectrum: The illumination is an `nthcomp` spectrum, with a photon index $\Gamma$ = 2.0 and an electron temperature $kT_e$ = 60 keV and blackbody temperture at $0.05$ keV
Click to expand plot script
```python
import matplotlib.pyplot as plt
import matplotlib as mpl
from astropy.io import fits
import numpy as np
import pandas as pd
import matplotlib.colors as colors
from matplotlib.ticker import FixedLocator, FixedFormatter
from matplotlib.colors import Normalize,LogNorm
from matplotlib.cm import ScalarMappable
mpl.rcParams['text.usetex'] = True
plt.style.use('seaborn-v0_8-ticks')
mpl.rcParams['font.family'] = 'serif'
mpl.rcParams['font.serif'] = 'Computer Modern'
mpl.rcParams['font.size'] = 20
colormap = 'viridis'
def to_roman(num):
"""Converts an integer to a Roman numeral string."""
val_syms = [
(10, "X"), (9, "IX"), (5, "V"), (4, "IV"),
(1, "I")
]
roman_num = ""
tens = num // 10
roman_num += "X" * tens
num %= 10
for val, sym in val_syms:
while num >= val:
roman_num += sym
num -= val
return roman_num
data = pd.read_csv('./temp_a7f0a841e1_dxi5000_abund.dat',sep="\s+",header=None).iloc[-200:]
tau = np.array(pd.to_numeric(data[0],errors='coerce'))
log_tau = np.log10(tau)
midpoints = (log_tau[:-1] + log_tau[1:]) / 2.0
first_boundary = log_tau[0] - (midpoints[0] - log_tau[0])
last_boundary = log_tau[-1] + (log_tau[-1] - midpoints[-1])
boundaries_log = np.concatenate([[first_boundary], midpoints, [last_boundary]])
tau_boundaries = 10**boundaries_log
def loadabund(path):
hdul= fits.open(path)
abund=hdul[1].data
return abund
def get_ionfrac(abund,ionname):
names = [name for name in abund.columns.names if name.startswith(ionname)]
ionfrac = np.zeros((len(names),len(tau)))
for i,name in enumerate(names):
ionfrac[i] = abund[name]
ionfrac /= ionfrac.sum(axis=0)
return ionfrac
def getinfo(abund):
fefrac = get_ionfrac(abund,'fe_')
ofrac = get_ionfrac(abund,'o_')
cfrac = get_ionfrac(abund,'c_')
ioninfo = {
'fe':{
"major_ions": [1, 5, 10, 15, 20, 26],
"major_locs": [ion + 0.5 for ion in [1, 5, 10, 15, 20, 26]],
"major_labels":[f"Fe {to_roman(i)}" for i in [1, 5, 10, 15, 20, 26]]
},
'o':{
"major_ions": [1,2,3,4,5,6,7,8],
"major_locs": [ion + 0.5 for ion in [1,2,3,4,5,6,7,8]],
"major_labels":[f"O {to_roman(i)}" for i in [1,2,3,4,5,6,7,8]]
},
"c":{
"major_ions": [1,2,3,4,5,6],
"major_locs": [ion + 0.5 for ion in [1,2,3,4,5,6]],
"major_labels":[f"C {to_roman(i)}" for i in [1,2,3,4,5,6]]
}
}
return fefrac,ofrac,cfrac,ioninfo
def plotcolormap(ioninfo,ax,frac,ionname):
ion_levels = np.arange(1, frac.shape[0] + 2)
im = ax.pcolormesh(tau_boundaries, ion_levels, frac,
shading = 'flat',
cmap = colormap,
norm = colors.LogNorm(vmin=1e-4, vmax=1.0))
ax.set_xscale('log')
major_ions = ioninfo[ionname]['major_ions']
major_locs = ioninfo[ionname]['major_locs']
major_labels = ioninfo[ionname]['major_labels']
ax.yaxis.set_major_locator(FixedLocator(major_locs))
ax.yaxis.set_major_formatter(FixedFormatter(major_labels))
all_ions = np.arange(1, frac.shape[0] + 1)
minor_locs = [ion + 0.5 for ion in all_ions]
ax.yaxis.set_minor_locator(FixedLocator(minor_locs))
ax.tick_params(axis='y', which='major', length=7, width=1.5)
ax.tick_params(axis='y', which='minor', length=4, width=1)
return im
fig, ax = plt.subplots(3,3,figsize=(12, 9),layout="constrained",sharex=True)
fig.supylabel(r'Ionization State')
paths = ['abund_a7f0a841e1_dxi5000_abund.fits','abund_0eae9aa8c6_dxi5000_abund.fits','abund_a2cf2f5d08_dxi5000_abund.fits']
zetas = ['4.0','3.0','2.0']
cmap = mpl.cm.get_cmap('viridis')
norm = LogNorm(vmin=1e-4, vmax=1.0)
sm = ScalarMappable(cmap=cmap, norm=norm)
sm.set_array([])
for i,path in enumerate(paths):
abund = loadabund(path)
fefrac,ofrac,cfrac,ioninfo = getinfo(abund)
plotcolormap(ioninfo,ax[i][0],fefrac,'fe')
plotcolormap(ioninfo,ax[i][1],ofrac,'o')
im=plotcolormap(ioninfo,ax[i][2],cfrac,'c')
ax[i][0].text(0.05, 0.15, rf'log$\xi$ = {zetas[i]}', transform=ax[i][0].transAxes, verticalalignment='top', horizontalalignment='left')
if i==len(paths)-1:
for j in range(3):
ax[i][j].set_xlabel(r"$\tau_{T}$")
cbar = fig.colorbar(im, ax=ax, orientation='horizontal',
aspect=40, pad=0.04)
cbar.set_label("Ionization Fraction", fontsize=20)
plt.show()
```
```{figure} images/abund.svg
:width: 100%
:align: center
:name: fig-abund
Ionic fractions of iron (Fe), oxygen (O), and carbon (C) as a function of Thomson optical depth for three different ionization parameters: $\log\xi = 4.0$ (top row), $\log\xi = 3.0$ (middle row), and $\log\xi = 2.0$ (bottom row). The other parameters are the same as in Figure~\ref{fig:xivstau}.
```
## Heating and cooling rate
we compare reflection spectra for hydrogen densities of 10$^{15}$, 10$^{16}$, and 10$^{17}$ cm$^{-3}$ under `nthcomp` illumination. Other parameters are set at their default values.
Click to expand plot script
```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import json
import os
from rdata import read_spec,read_spec2,read_temp,read_spec5000,readabund
from matplotlib.colors import Normalize
from matplotlib.cm import ScalarMappable
import matplotlib.cm as cm
import matplotlib.colors as mcolors
from datetime import datetime, time
import sys
def fix_scientific(x):
if isinstance(x, str) and '-' in x and 'E' not in x and 'e' not in x:
parts = x.split('-')
if len(parts) == 2 and parts[0].replace('.', '').isdigit() and parts[1].isdigit():
return float(parts[0] + 'e-' + parts[1])
return float(x)
def convert_filepath(filepath):
new_filepath = filepath.replace('spec_', 'abund_').replace('.dat', '.fits')
return new_filepath
plt.style.use('seaborn-v0_8-ticks')
mpl.rcParams['text.usetex'] = True
mpl.rcParams['text.latex.preamble'] = r'\usepackage{mathptmx}'
mpl.rcParams['font.family'] = 'serif'
mpl.rcParams['font.size'] = 20
ERGSEV = 1.60217662e-12
METADATA_DIR = 'metadata'
if not os.path.isdir(METADATA_DIR):
raise FileNotFoundError(f"元数据目录 '{METADATA_DIR}' 不存在。")
datas = os.listdir(METADATA_DIR)
def normtopflux(nh,flux):
factor = 1e15/nh
return flux*factor
def normhc(nh, data):
factor = (nh/1e15)**2
data/=factor
return data
plot_data_list = []
for data_filename in datas:
with open(os.path.join(METADATA_DIR, data_filename), 'r') as f:
try:
data = json.load(f)
params = data['parameters']
if params.get('diy_tag') == 'DNHX' and float(params['nh'])!=1e21:
try:
specfile = params['op_spec']
abundfile = convert_filepath(specfile)
tempfile = params['op_temp']
nh = float(params['nh'])
# read out spec
ener,top,bot,inte,flux = read_spec5000(specfile)
# norm flux
flux = normtopflux(nh,flux)
# read temperature
tau,temp = read_temp(tempfile)
# read heating-cooling
comh,comc,brec,toth,totc = readabund(abundfile)
comh = normhc(nh,comh)
comc = normhc(nh,comc)
brec = normhc(nh,brec)
toth = normhc(nh,toth)
totc = normhc(nh,totc)
task_info = {
'ener': ener,
'eemo': flux,
'tau' : tau,
'temp': temp,
'top': top,
'nh' : float(params['nh']),
'compton heating':comh,
'compton cooling':comc,
'brems cooling':brec,
'total heating':toth,
'total cooling':totc
}
plot_data_list.append(task_info)
except Exception as e:
print(f"fail {params['op_spec']}: {e}")
continue
except (json.JSONDecodeError, KeyError, TypeError) as e:
print(f"Warning: skip {data_filename}: {e}")
continue
if not plot_data_list:
print("Error: no 'diy_tag ")
exit()
plot_data_list.sort(key=lambda item: item['nh'])
nh_values = np.log10(np.array([item['nh'] for item in plot_data_list]))
cmap = cm.viridis
norm = mcolors.LogNorm(vmin=nh_values.min(), vmax=nh_values.max())
model_colors = {d: mcolors.to_hex(cmap(norm(d))) for d in nh_values}
labels = {
1e15: r'logn$_h$=15',
1e16: r'logn$_h$=16',
1e17: r'logn$_h$=17',
1e18: r'logn$_h$=18',
1e19: r'logn$_h$=19',
1e20: r'logn$_h$=20',
1e21: r'logn$_h$=21',
1e22: r'logn$_h$=22'
}
fig, ax = plt.subplots(
1, 2,
figsize=(3.4, 5.0),
constrained_layout=True
)
lw = 2.0
traget_inci = np.cos(np.deg2rad(45))
for i,item in enumerate(plot_data_list):
color = model_colors[np.log10(item['nh'])]
label = labels[item['nh']]
energ = item['ener']
flux = item['eemo']
top = item['top']
tau=item['tau']
temp = item['temp']
comh = item['compton heating']
comc = item['compton cooling']
brec = item['brems cooling']
toth = item['total heating']
totc = item['total cooling']
# if i==0:
# ax[0].loglog(ener, top, color='k',lw=lw,linestyle='--')
ax[0].loglog(tau, comh , color=color, lw=1,label=label)
ax[1].loglog(tau, comc,color=color,lw=lw,label=label)
ax[0].set_ylabel(r'Rate')
ax[0].set_xlim(0.0001,10)
ax[1].legend(loc='upper right')
ax[0].set_ylim(1e3,1e7)
ax[0].set_xticks([0.001,0.01,0.1,1,10])
ax[0].set_xticklabels(['0.001','0.01','0.1','1','10'])
ax[0].set_xlabel(r"Thomson depth $\tau_T$")
ax[0].text(0.05, 0.95, rf'Compton Heating', transform=ax[0].transAxes, verticalalignment='top', horizontalalignment='left')
ax[1].set_ylabel(r'Rate')
ax[1].set_xlim(0.0001,10)
ax[1].set_xticks([0.001,0.01,0.1,1,10])
ax[1].set_xticklabels(['0.001','0.01','0.1','1','10'])
ax[1].set_ylim(10**3,1e7)
ax[1].text(0.05, 0.95, rf'Compton Cooling', transform=ax[1].transAxes, verticalalignment='top', horizontalalignment='left')
ax[1].set_xlabel(r"Thomson depth $\tau_T$")
plt.show()
```
```{figure} images/comhc.svg
:width: 100%
:align: center
:name: fig-comhc
Compton heating and cooling rates
```
```{figure} images/PIff.svg
:width: 100%
:align: center
:name: fig-PIff
Photoionization and bremsstrahlung heating rates, along with the bremsstrahlung cooling rate.
```
## Relativistic spectrum
The `reldao` and `reldaoA` models are developed within the framework of `relxill` v2.5 {cite:p}`2014ApJ...782...76G,2025ApJ...989..168H` and `relxillA` {cite:p}`2025ApJ...989..168H`. Currently, as the full table models have not yet been generated, `reldao` serves as a prototype rather than a finalized relativistic reflection model. The code will be publicly released upon the completion of the `DAO` table models. Additionally, the relativistic spectrum is computed using the convolution model `relconv` {cite:p}`2010MNRAS.409.1534D` within `XSPEC` {cite:p}`1996ASPC..101...17A`, yielding the combined model `relconv*dao`.
The primary distinction between the convolution model `relconv*dao` and the direct implementations, `reldao` and `reldaoA`, lies in the treatment of angular dependence. The convolution model operates on the angle-averaged flux, whereas `reldao` accounts for the full angular distribution by integrating over all local emission angles at a given incidence angle. Furthermore, `relxillA` incorporates a more comprehensive treatment of angular effects. In the specific case of `reldao`, the observed flux is calculated as:
$$
F_{obs} (E_{obs}) &= \frac{1}{D^2} \int_{R_{in}}^{R_{out}} dr_e \int_0^1 g^* \frac{\pi r_e g^2}{\sqrt{g^*(1-g^*)}} \left[f^{(1)}(g^*,r_e,\theta_{obs})+f^{(2)}(g^*,r_e,\theta_{obs})\right] \\ &\times \epsilon(r_e) \langle\bar{I_e}(E_e)\rangle
$$
and for `reldaoA`, the model calculates the observed flux as:
$$
\begin{aligned}
F_{obs} (E_{obs}) &= \frac{1}{D^2} \sum_{i=0}^9 \int_{R_{in}}^{R_{out}}dr_e\int_0^1 dg^* \frac{\pi r_eg^2}{\sqrt{g^*(1-g^*)}}\left[f^{(1)}(g^*,r_e,\theta_{obs})+f^{(2)}(g^*,r_e,\theta_{obs})\right] \\
&\times\epsilon(r_e)\bar{I}(E_e,r_e,\bar{\theta_e})\Theta(\theta_e-\theta_i)\Theta(\theta_{i+1}-\theta_e)
\end{aligned}
$$
All the physical parameters above have been explained in detail in section 2 of {cite:t}`2025ApJ...989..168H`.
```{figure} images/reldaonthcomp.svg
:name: fig-reldao
:width: 100%
:align: center
Relativistic reflection spectra calculated with `relconv*dao` (green dash-dot line), `reldao` (blue dashed line), and `reldaoA` (red solid line). The gas is illuminated by `nthcomp` with $\Gamma = 2.4$ (left column) and $\Gamma = 1.4$ (right column). Ionization parameter is set to 3.0 for both cases. The inclination angle is $\theta_{i} = 30$ deg, and all other parameters in `relxill` framework (e.g., spin, radius, etc.) are fixed at their default values.