Production Rate

After fitting a fluorescence model, the column density \(\log_{10} N\) (\(N\) in molecules cm-2) can be converted to a molecular production rate \(Q\) (molecules s-1) using one of two approaches:

  • Haser model — models a spherically expanding coma with parent and daughter photodissociation scale lengths.

  • ρ-1 profile — assumes a pure radial-outflow column-density profile \(N(\rho) \propto \rho^{-1}\) and uses a pre-computed aperture integral.

The examples below build a short MCMC run first so that posterior samples are available. To skip the sampling step, pass use_samples=False to use the median logN directly (no error estimate is returned in that case); if you use this approach, there is no need to fit.

Sections


Let’s prepare a fitted model from which we will compute production rates.

from cometspec.helper import open_kurucz_irradiance, open_hall_anderson_irradiance
from cometspec import FluorescenceModel
from matplotlib import pyplot as plt
import numpy as np
import pandas as pd

irradiance = open_kurucz_irradiance()

model = FluorescenceModel(
    window=(3870, 3885),
    pumping=irradiance,
    isotopologues="12C14N",
    systems=['BX', 'AX_dv1'],
    sigma=0.05,
    logQ=-1,
    logN=14.0,
    T=200,
    v_kms=0,
    lsf_method="Gauss",
    wave_col="WAVE",
    flux_col="FLUX",
    error_col="ERR",
    continuum_col="CONTINUUM",
)

# add noise
data_flux = model.best_model + np.random.normal(0, np.max(model.best_model)*0.05,
                                                    size=model.model_wave.shape)
data = pd.DataFrame({'WAVE': model.model_wave,
                    'FLUX': data_flux,
                    'ERR':np.max(model.best_model)*0.05,
                    'CONTINUUM': np.zeros_like(model.model_wave)})

results = model.fit_mcmc(
                data=data,
                nwalkers=10,
                nsteps=500,
                priors={
                    "logN":  (12.0, 16.0),
                    "T":     (50.0, 300.0),
                    "v_kms": (-2.0, 2.0),
                },
                make_plots=False,
                )

Haser model

compute_production_rate() uses the sbpy Haser implementation. You must supply the geocentric distance, aperture geometry, CN scale lengths, and the outflow velocity.

Typical CN scale lengths at 1 AU are: parent ~ 1.3 × 104 km, daughter ~ 2.1 × 105 km [1]. Both scale as \(r_h^2\). The outflow velocity can be estimated as \(v = 0.85 r_h^{-0.5}\) km s-1 [2], where \(r_h\) is the heliocentric distance in AU.

rh = 1.0   # your heliocentric distance [AU]

logQ, logQ_err = model.compute_production_rate(
    delta_au=1.0, # your geocentric distance [AU]
    aperture={"type": "circular", "radius_arcsec": 5.0},
    parent_length_km=1.3e4 * rh**2,
    daughter_length_km=2.1e5 * rh**2,
    v_outflow_km_s=0.85 * rh**(-0.5),
)
print(f"log Q(CN) = {logQ:.2f} ± {logQ_err:.2f}  [s^-1]")

Rectangular (slit) apertures are also supported:

logQ_slit, logQ_slit_err = model.compute_production_rate(
    delta_au=1.0,
    aperture={"type": "rectangular", "width_arcsec": 2.0, "length_arcsec": 10.0},
    parent_length_km=1.3e4 * rh**2,
    daughter_length_km=2.1e5 * rh**2,
    v_outflow_km_s=0.85 * rh**(-0.5),
)
print(f"log Q(CN, slit) = {logQ_slit:.2f} ± {logQ_slit_err:.2f}")

The production rate and its error are stored in the attributes model.q and model.q_err after calling compute_production_rate.


ρ-1 profile via aperture integral

For a \(N(\rho) \propto \rho^{-1}\) profile where \(\rho\) is the radial distance from the nucleus, first compute the geometric aperture integral G with compute_aperture_integral(), then pass it to compute_production_rate_from_profile().

G = model.compute_aperture_integral(
    aperture={"type": "circular", "radius_arcsec": 5.0},
    delta_au=1.0,
)
print("Aperture integral G:", G)

logQ_prof, logQ_prof_err = model.compute_production_rate_from_profile(
    G=G,
    delta_au=1.0,
    aperture={"type": "circular", "radius_arcsec": 5.0},
    v_outflow_km_s=0.85 * rh**(-0.5),
)
print(f"log Q (rho^-1 profile) = {logQ_prof:.2f} ± {logQ_prof_err:.2f}")

Multi-isotopologue production rates

When multiple isotopologues are fitted, compute_production_rate() returns a dict keyed by isotopologue name. You can also use the attribute model.q to access the production rates and errors for each isotopologue after calling compute_production_rate.

# Two-iso fit
model = FluorescenceModel(
    pumping=irradiance,
    isotopologues=["12C14N", "13C14N"], systems="BX",
    logN_by_iso={"12C14N": 14, "13C14N": 13.0},
    T=100.0, sigma=0.02, logQ=-1, lsf_method="Gauss",
    wave_col="WAVE", flux_col="FLUX", error_col="ERR",
    continuum_col="CONTINUUM",
)
data_flux = model.best_model + np.random.normal(0, np.max(model.best_model)*0.05,
                                                    size=model.model_wave.shape)
data = pd.DataFrame({'WAVE': model.model_wave,
                    'FLUX': data_flux,
                    'ERR':np.max(model.best_model)*0.05,
                    'CONTINUUM': np.zeros_like(model.model_wave)})


model.fit_mcmc(
    data=data,
    nwalkers=20, nsteps=300,
    priors={
        "logN_12C14N": (12.0, 16.0),
        "logN_13C14N": (11.0, 14.0),
        "T":           (50.0, 300.0),
    },
    make_plots=False, progress=False,
)

rates = model.compute_production_rate(
    delta_au=1.0,
    aperture={"type": "circular", "radius_arcsec": 5.0},
    parent_length_km=1.3e4 * rh**2,
    daughter_length_km=2.1e5 * rh**2,
    v_outflow_km_s=0.85 * rh**(-0.5),
)
for iso, (logQ_i, logQ_i_err) in rates.items():
    print(f"log Q({iso}) = {logQ_i:.2f} ± {logQ_i_err:.2f}")

Note

If you have multiple isotopologues and want to use use_samples=False, you need to have both present in logN_by_iso. On the other hand, if multiple isotopologues are present and you want to use use_samples=True, you will need to fit both isotopologues; otherwise the code raises an error. However, you can work around this by adding the missing isotopologue later to logN_by_iso, appending the missing logN_<isotopologue> at the end of param_keys, and adding an array of the repeated value to samples_pruned. An example of this can be found in the Jupyter notebook WorkFlow.ipynb of the GitHub repository (link).


Seeing-corrected production rates

add_slit_loss_error() inflates the column-density uncertainty to account for seeing-induced slit losses. Call it on the fitted model after compute_production_rate. This will also compute the slit-loss error of logN.

print('Before adding slit-loss error:')
for iso in model.q:
    print(f"log Q({iso}) = {model.q[iso]:.3f} ± {model.q_err[iso]:.3f}")
for iso in model.logN_by_iso:
    print(f"log N({iso}) = {model.logN_by_iso[iso]:.2f} ± {model.logN_err_by_iso[iso][0]:.2f}") # for N you have up and low error, we just show 1

model.add_slit_loss_error(
lambda_nm=388.3, # the wavelength of the fitted band or line [nm]
aperture={"type": "circular", "radius_arcsec": .5},
            # small aperture to illustrate the effect
eps_min_arcsec_500=0.7,   # best-case seeing at 500 nm [arcsec]
eps_max_arcsec_500=1.5,   # worst-case seeing at 500 nm [arcsec]
zmin_deg=20, # minimum zenith angle of observations [degrees]
zmax_deg=60, # maximum zenith angle of observations [degrees]
)

print('After adding slit-loss error:')
for iso in model.q:
    print(f"log Q({iso}) = {model.q[iso]:.3f} ± {model.q_err[iso]:.3f}")
for iso in model.logN_by_iso:
    print(f"log N({iso}) = {model.logN_by_iso[iso]:.2f} ± {model.logN_err_by_iso[iso][0]:.2f}") # for N you have up and low error, we just show 1

If only one isotopologue is fitted, you have logN and logN_err. We encourage you to read the documentation of add_slit_loss_error() for more details on the method and its assumptions. Since this method adds the initial error in quadrature with the slit-loss error, the slit-loss error is not recalculated if add_slit_loss_error is called multiple times, in order to avoid accumulating it. If you want to recalculate it, you will need to either 1) fit again, or 2) reset the attributes q_seeing_corrected = False and logN_seeing_corrected = False and run the production rate calculation again (if the latter is not applied, the error will start accumulating repeatedly).

References