Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions plan_histories/70.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Goal

Extend photon `PowerLaw` opacities to support an optional frequency power law
while preserving the legacy gray behavior when no frequency exponent is
provided.

## Functional Plan

- Add optional `nu_exp` and `nu_ref` parameters to photon `PowerLaw`.
- Apply the frequency power law only to the monochromatic opacity and
monochromatic emissivity APIs.
- Preserve the existing legacy constructor calls by keeping the default
frequency behavior equivalent to `nu_exp = 0`.
- Keep the `NonCGSUnits` wrapper path working for the frequency-dependent
`PowerLaw`.
- Add focused regression coverage that checks:
- the monochromatic absorption coefficient matches the expected
`(\nu / \nu_ref)^{\nu_exp}` scaling
- the monochromatic emissivity remains `j_\nu = \alpha_\nu B_\nu`
- the same frequency dependence survives the `NonCGSUnits` wrapper

## Scope Boundaries

- Do not add integrated `Emissivity` or `NumberEmissivity` support for
`nu_exp != 0`.
112 changes: 94 additions & 18 deletions singularity-opac/photons/powerlaw_opacity_photons.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// ======================================================================
// © 2024. Triad National Security, LLC. All rights reserved. This
// © 2024-2026. Triad National Security, LLC. All rights reserved. This
// program was produced under U.S. Government contract
// 89233218CNA000001 for Los Alamos National Laboratory (LANL), which
// is operated by Triad National Security, LLC for the U.S.
Expand All @@ -16,6 +16,8 @@
#ifndef SINGULARITY_OPAC_PHOTONS_POWERLAW_OPACITY_PHOTONS_
#define SINGULARITY_OPAC_PHOTONS_POWERLAW_OPACITY_PHOTONS_

// This file was made in part with generative AI.

#include <cassert>
#include <cmath>
#include <cstdio>
Expand All @@ -33,26 +35,61 @@ class PowerLawOpacity {
using PC = pc;

PowerLawOpacity() = default;
PowerLawOpacity(const Real kappa0, const Real rho_exp, const Real temp_exp)
: kappa0_(kappa0), rho_exp_(rho_exp), temp_exp_(temp_exp) {}
PowerLawOpacity(const Real kappa0, const Real rho_exp, const Real temp_exp,
const Real nu_exp = 0., const Real nu_ref = 1., const Real nu_off = 0.,
const Real rho_ref = 1., const Real rho_off = 0.,
const Real temp_ref = 1., const Real temp_off = 0.,
const bool do_stim_emit = false)
: PowerLawOpacity(PlanckDistribution<pc>{}, kappa0, rho_exp, temp_exp,
nu_exp, nu_ref, nu_off, rho_ref, rho_off, temp_ref,
temp_off, do_stim_emit) {}
PowerLawOpacity(const PlanckDistribution<pc> &dist, const Real kappa0,
const Real rho_exp, const Real temp_exp)
: dist_(dist), kappa0_(kappa0), rho_exp_(rho_exp), temp_exp_(temp_exp) {}
const Real rho_exp, const Real temp_exp,
const Real nu_exp = 0., const Real nu_ref = 1., const Real nu_off = 0.,
const Real rho_ref = 1., const Real rho_off = 0.,
const Real temp_ref = 1., const Real temp_off = 0.,
const bool do_stim_emit = false)
: dist_(dist), kappa0_(kappa0), rho_exp_(rho_exp), temp_exp_(temp_exp),
rho_ref_(rho_ref), rho_off_(rho_off), temp_ref_(temp_ref), temp_off_(temp_off),
nu_exp_(nu_exp), nu_ref_(nu_ref), nu_off_(nu_off), do_stim_emit_(do_stim_emit) {
if (!(rho_ref_ > 0.)) {
OPAC_ERROR("PowerLawOpacity: rho_ref must be positive");
}
if (!(temp_ref_ > 0.)) {
OPAC_ERROR("PowerLawOpacity: temp_ref must be positive");
}
if (!(nu_ref_ > 0.)) {
OPAC_ERROR("PowerLawOpacity: nu_ref must be positive");
}
if (rho_off_ < 0.) {
OPAC_ERROR("PowerLawOpacity: rho_off must be nonnegative");
}
if (temp_off_ < 0.) {
OPAC_ERROR("PowerLawOpacity: temp_off must be nonnegative");
}
if (nu_off_ < 0.) {
OPAC_ERROR("PowerLawOpacity: nu_off must be nonnegative");
}
}

PowerLawOpacity GetOnDevice() { return *this; }
PORTABLE_INLINE_FUNCTION
int nlambda() const noexcept { return 0; }
PORTABLE_INLINE_FUNCTION
void PrintParams() const noexcept {
printf("Power law opacity. kappa0 = %g rho_exp = %g temp_exp = %g\n",
kappa0_, rho_exp_, temp_exp_);
printf("Power law opacity. kappa0 = %g rho_exp = %g temp_exp = %g "
"nu_exp = %g nu_ref = %g nu_off = %g rho_ref = %g rho_off = %g"
"temp_ref = %g temp_off = %g do_stim_emit = %d\n",
kappa0_, rho_exp_, temp_exp_, nu_exp_, nu_ref_,
nu_off_, rho_ref_, rho_off_, temp_ref_, temp_off_,
do_stim_emit_);
}
inline void Finalize() noexcept {}

PORTABLE_INLINE_FUNCTION
Real AbsorptionCoefficient(const Real rho, const Real temp, const Real nu,
Real *lambda = nullptr) const {
return rho * (kappa0_ * std::pow(rho, rho_exp_) * std::pow(temp, temp_exp_));
return rho * OpacityScale_(rho, temp, nu);
}

template <typename FrequencyIndexer, typename DataIndexer>
Expand Down Expand Up @@ -86,9 +123,7 @@ class PowerLawOpacity {
Real EmissivityPerNuOmega(const Real rho, const Real temp, const Real nu,
Real *lambda = nullptr) const {
Real Bnu = dist_.ThermalDistributionOfTNu(temp, nu, lambda);
return rho *
(kappa0_ * std::pow(rho, rho_exp_) * std::pow(temp, temp_exp_)) *
Bnu;
return rho * OpacityScale_(rho, temp, nu) * Bnu;
}

template <typename FrequencyIndexer, typename DataIndexer>
Expand Down Expand Up @@ -120,15 +155,34 @@ class PowerLawOpacity {
PORTABLE_INLINE_FUNCTION
Real Emissivity(const Real rho, const Real temp,
Real *lambda = nullptr) const {
Real B = dist_.ThermalDistributionOfT(temp, lambda);
return rho *
(kappa0_ * std::pow(rho, rho_exp_) * std::pow(temp, temp_exp_)) * B;
// Once the opacity depends on frequency, the total emissivity is no longer
// the gray factorization alpha(T, rho) * ThermalDistributionOfT(T). This
// class intentionally supports only the monochromatic frequency-resolved
// emissivity APIs in that case. Supporting the fully integrated form would
// require carrying factorial/Gamma-style moments and Riemann zeta
// functions for the frequency-dependent power law.
if (nu_exp_ != 0.) {
OPAC_ERROR("PowerLawOpacity: total emissivity is only supported for "
"nu_exp = 0. Use EmissivityPerNuOmega or EmissivityPerNu "
"for frequency-dependent power laws.");
}
return rho * OpacityPrefactor_(rho, temp) *
dist_.ThermalDistributionOfT(temp, lambda);
}

PORTABLE_INLINE_FUNCTION
Real NumberEmissivity(const Real rho, const Real temp,
Real *lambda = nullptr) const {
return (kappa0_ * std::pow(rho, rho_exp_) * std::pow(temp, temp_exp_)) *
// Same limitation as Emissivity(): for frequency-dependent power laws, the
// integrated number emissivity is not treated as a gray closed-form API,
// and full support would likewise require factorial/Gamma-style moments
// and Riemann zeta functions.
if (nu_exp_ != 0.) {
OPAC_ERROR("PowerLawOpacity: total number emissivity is only supported "
"for nu_exp = 0. Use EmissivityPerNuOmega or EmissivityPerNu "
"for frequency-dependent power laws.");
}
return OpacityPrefactor_(rho, temp) *
dist_.ThermalNumberDistributionOfT(temp, lambda);
}

Expand Down Expand Up @@ -178,9 +232,31 @@ class PowerLawOpacity {
}

private:
Real kappa0_; // Opacity scale. Units of cm^2/g
Real rho_exp_; // Power law index of density
Real temp_exp_; // Power law index of temperature
PORTABLE_INLINE_FUNCTION
Real OpacityScale_(const Real rho, const Real temp, const Real nu) const {
const Real freq_plaw = std::pow((nu + nu_off_) / nu_ref_, nu_exp_);
const Real stim_fact = do_stim_emit_ ? -std::expm1(-(pc::h * nu / (pc::kb * temp))) : 1.0;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this brings in units... can you double check that all the conversions are threaded through correctly to get the correctly unit-informed Planck and Boltzmann constant?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch - will do...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is the CGS side of the class, it seems nu and temp should be in CGS by the time they reach this function, so that leaves the question of whether pc::h and pc::kb are properly non-dimensionalizing the exponential argument (or equivalently, what the actual units of nu is - e.g. energy or 1/time). If the nu here is assumed consistent with the nu taken by the EmissivityPerNuPerOmega function, it seems this should be correct: EmissivityPerNuPerOmega calls ThermalDistributionOfNu which takes nu directly and computes the nondimensional parameter x = pc::h * nu / (pc::kb * temp) as in the stimulated emission formula here. Let me know if this reasoning misses something.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following on this, noticed that the parameters rho_ref and temp_ref must be in CGS when the constructor is invoked. That is maybe a bit cumbersome - noted this in the comments next to these class members in each of the power law classes (also made a note that nu_ref has to have units of 1/s, which I think is true for the same reason unless kappa0 absorbs the unit).

return OpacityPrefactor_(rho, temp) * freq_plaw * stim_fact;
}

PORTABLE_INLINE_FUNCTION
Real OpacityPrefactor_(const Real rho, const Real temp) const {
const Real rhom = (rho + rho_off_) / rho_ref_;
const Real tempm = (temp + temp_off_) / temp_ref_;
return kappa0_ * std::pow(rhom, rho_exp_) * std::pow(tempm, temp_exp_);
}

Real kappa0_; // Opacity scale. Units depend on nu_exp and nu_ref.
Real rho_exp_; // Power law index of density
Real temp_exp_; // Power law index of temperature
Real rho_ref_; // Density normalization for rho_exp. Units of g/cm^3
Real rho_off_; // Density offset (same units as rho_ref). Units of g/cm^3
Real temp_ref_; // Temperature normalization for temp_exp. Units of K
Real temp_off_; // Temperature offset (same units as temp_ref). Units of K
Real nu_exp_; // Power law index of frequency
Real nu_ref_; // Frequency normalization for nu_exp. Units of 1/s
Real nu_off_; // Frequency offset (same units as nu_ref). Units of 1/s
bool do_stim_emit_; // indicator to use stimulated (LTE) emission factor
PlanckDistribution<pc> dist_;
};

Expand Down
125 changes: 125 additions & 0 deletions singularity-opac/photons/powerlaw_s_opacity_photons.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// ======================================================================
// © 2026. Triad National Security, LLC. All rights reserved. This
// program was produced under U.S. Government contract
// 89233218CNA000001 for Los Alamos National Laboratory (LANL), which
// is operated by Triad National Security, LLC for the U.S.
// Department of Energy/National Nuclear Security Administration. All
// rights in the program are reserved by Triad National Security, LLC,
// and the U.S. Department of Energy/National Nuclear Security
// Administration. The Government is granted for itself and others
// acting on its behalf a nonexclusive, paid-up, irrevocable worldwide
// license in this material to reproduce, prepare derivative works,
// distribute copies to the public, perform publicly and display
// publicly, and to permit others to do so.
// ======================================================================

#ifndef SINGULARITY_OPAC_PHOTONS_POWERLAW_S_OPACITY_PHOTONS_
#define SINGULARITY_OPAC_PHOTONS_POWERLAW_S_OPACITY_PHOTONS_

// This file was partly copied from a file made in part with generative AI.

#include <cassert>
#include <cmath>
#include <cstdio>

#include <ports-of-call/portability.hpp>
#include <singularity-opac/base/opac_error.hpp>

namespace singularity {
namespace photons {

template <typename pc = PhysicalConstantsCGS>
class PowerLawSOpacity {
public:
PowerLawSOpacity() = default;
PowerLawSOpacity(const Real kappa0, const Real rho_exp, const Real temp_exp,
const Real nu_exp = 0., const Real nu_ref = 1., const Real nu_off = 0.,
const Real rho_ref = 1., const Real rho_off = 0.,
const Real temp_ref = 1., const Real temp_off = 0.,
const Real avg_particle_mass = 1.)
: kappa0_(kappa0), rho_exp_(rho_exp), temp_exp_(temp_exp),
rho_ref_(rho_ref), rho_off_(rho_off), temp_ref_(temp_ref), temp_off_(temp_off),
nu_exp_(nu_exp), nu_ref_(nu_ref), nu_off_(nu_off), apm_(avg_particle_mass) {
if (!(nu_ref_ > 0.)) {
OPAC_ERROR("PowerLawSOpacity: nu_ref must be positive");
}
if (!(temp_ref_ > 0.)) {
OPAC_ERROR("PowerLawSOpacity: temp_ref must be positive");
}
if (!(nu_ref_ > 0.)) {
OPAC_ERROR("PowerLawSOpacity: nu_ref must be positive");
}
if (rho_off_ < 0.) {
OPAC_ERROR("PowerLawSOpacity: rho_off must be nonnegative");
}
if (temp_off_ < 0.) {
OPAC_ERROR("PowerLawSOpacity: temp_off must be nonnegative");
}
if (nu_off_ < 0.) {
OPAC_ERROR("PowerLawSOpacity: nu_off must be nonnegative");
}
}

PowerLawSOpacity GetOnDevice() { return *this; }
PORTABLE_INLINE_FUNCTION
int nlambda() const noexcept { return 0; }
PORTABLE_INLINE_FUNCTION
void PrintParams() const noexcept {
printf("Power law scattering opacity. kappa0 = %g rho_exp = %g temp_exp = %g "
"nu_exp = %g nu_ref = %g nu_off = %g rho_ref = %g rho_off = %g"
"temp_ref = %g temp_off = %g avg particle mass = %g\n",
kappa0_, rho_exp_, temp_exp_, nu_exp_, nu_ref_,
nu_off_, rho_ref_, rho_off_, temp_ref_, temp_off_,
apm_);
}
inline void Finalize() noexcept {}

PORTABLE_INLINE_FUNCTION
Real TotalCrossSection(const Real rho, const Real temp, const Real nu,
Real *lambda = nullptr) const {
return OpacityScale_(rho, temp, nu);
}

PORTABLE_INLINE_FUNCTION
Real DifferentialCrossSection(const Real rho, const Real temp, const Real nu,
const Real mu, Real *lambda = nullptr) const {
// assumed isotropic, elastic
return OpacityScale_(rho, temp, nu) / (4. * M_PI);
}

PORTABLE_INLINE_FUNCTION
Real TotalScatteringCoefficient(const Real rho, const Real temp,
const Real nu, Real *lambda = nullptr) const {
return (rho / apm_) * OpacityScale_(rho, temp, nu);
}

private:
PORTABLE_INLINE_FUNCTION
Real OpacityScale_(const Real rho, const Real temp, const Real nu) const {
return OpacityPrefactor_(rho, temp) * std::pow((nu + nu_off_) / nu_ref_, nu_exp_);
}

PORTABLE_INLINE_FUNCTION
Real OpacityPrefactor_(const Real rho, const Real temp) const {
const Real rhom = (rho + rho_off_) / rho_ref_;
const Real tempm = (temp + temp_off_) / temp_ref_;
return kappa0_ * std::pow(rhom, rho_exp_) * std::pow(tempm, temp_exp_);
}

Real kappa0_; // Opacity scale. Units depend on nu_exp and nu_ref.
Real rho_exp_; // Power law index of density
Real temp_exp_; // Power law index of temperature
Real rho_ref_; // Density normalization for rho_exp. Units of g/cm^3
Real rho_off_; // Density offset (same units as rho_ref). Units of g/cm^3
Real temp_ref_; // Temperature normalization for temp_exp. Units of K
Real temp_off_; // Temperature offset (same units as temp_ref). Units of K
Real nu_exp_; // Power law index of frequency
Real nu_ref_; // Frequency normalization for nu_exp. Units of 1/s
Real nu_off_; // Frequency offset (same units as nu_ref). Units of 1/s
Real apm_; // Mean molecular weight. Units of g
};

} // namespace photons
} // namespace singularity

#endif // SINGULARITY_OPAC_PHOTONS_POWERLAW_S_OPACITY_PHOTONS_
7 changes: 5 additions & 2 deletions singularity-opac/photons/s_opac_photons.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <singularity-opac/photons/gray_s_opacity_photons.hpp>
#include <singularity-opac/photons/non_cgs_s_photons.hpp>
#include <singularity-opac/photons/photon_s_variant.hpp>
#include <singularity-opac/photons/powerlaw_s_opacity_photons.hpp>
#include <singularity-opac/photons/thomson_s_opacity_photons.hpp>

namespace singularity {
Expand All @@ -29,9 +30,11 @@ namespace photons {
using ScaleFreeS = GraySOpacity<PhysicalConstantsUnity>;
using GrayS = GraySOpacity<>;
using ThomsonS = ThomsonSOpacity<>;
using PowerLawS = PowerLawSOpacity<>;

using SOpacity = impl::S_Variant<ScaleFreeS, GrayS, ThomsonS,
NonCGSUnitsS<GrayS>, NonCGSUnitsS<ThomsonS>>;
using SOpacity = impl::S_Variant<ScaleFreeS, GrayS, ThomsonS, PowerLawS,
NonCGSUnitsS<GrayS>, NonCGSUnitsS<ThomsonS>,
NonCGSUnitsS<PowerLawS>>;

} // namespace photons
} // namespace singularity
Expand Down
Loading
Loading