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
1 change: 1 addition & 0 deletions docs/source/pythonapi/stats.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Univariate Probability Distributions
:template: myfunction.rst

openmc.stats.delta_function
openmc.stats.fusion_neutron_spectrum
openmc.stats.muir

Angular Distributions
Expand Down
136 changes: 134 additions & 2 deletions openmc/stats/univariate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
from abc import ABC, abstractmethod
from collections import defaultdict
from collections.abc import Iterable, Sequence
from copy import deepcopy
from math import sqrt, pi, exp
from math import sqrt, pi, exp, log
from numbers import Real
from warnings import warn

Expand All @@ -14,6 +13,7 @@
import scipy

import openmc.checkvalue as cv
from openmc.data import atomic_mass, NEUTRON_MASS
from .._xml import get_elem_list, get_text
from ..mixin import EqualityMixin

Expand Down Expand Up @@ -1277,6 +1277,138 @@ def Muir(*args, **kwargs):
return muir(*args, **kwargs)


def fusion_neutron_spectrum(
ion_temp: float,
reactants: str = 'DD',
bias: Univariate | None = None
) -> Normal:
r"""Return a Gaussian energy distribution for fusion neutron emission.

Computes the mean energy and spectral width of the neutron energy spectrum
from thermonuclear fusion reactions in a plasma with Maxwellian ion velocity
distributions. The mean neutron energy is calculated as

.. math::

\langle E_n \rangle = E_0 + \Delta E_\text{th}

where :math:`E_0` is the neutron energy at zero ion temperature and
:math:`\Delta E_\text{th}` is the thermal peak shift due to the motion of
the reacting ions. The spectral width is characterized by the FWHM:

.. math::

W_{1/2} = \omega_0 (1 + \delta_\omega) \sqrt{T_i}

where :math:`\omega_0` is the width at the :math:`T_i \to 0` limit and
:math:`\delta_\omega` is a temperature-dependent correction term. Both
:math:`\Delta E_\text{th}` and :math:`\delta_\omega` are evaluated using
interpolation formulas from `Ballabio et al.
<https://doi.org/10.1088/0029-5515/38/11/310>`_: Table III for :math:`0 <
T_i \le 40` keV and Table IV for :math:`40 < T_i < 100` keV. The returned
distribution is a normal (Gaussian) approximation to the spectrum.

.. versionadded:: 0.15.4

Parameters
----------
ion_temp : float
Ion temperature of the plasma in [eV].
reactants : {'DD', 'DT'}
Fusion reactants. 'DD' corresponds to the D(d,n)\ :sup:`3`\ He reaction
and 'DT' to the T(d,n)\ :math:`\alpha` reaction.
bias : openmc.stats.Univariate, optional
Distribution for biased sampling.

Returns
-------
openmc.stats.Normal
Normal distribution with mean and standard deviation corresponding to
the first and second moments of the fusion neutron energy spectrum. Both
the mean and standard deviation are in [eV].

"""
if ion_temp < 0.0 or ion_temp > 100e3:
raise ValueError("Ion temperature must be between 0 and 100 keV.")

# Formulas from doi:10.1088/0029-5515/38/11/310
mn = NEUTRON_MASS
md = atomic_mass('H2')
ev_per_c2 = 931.49410372*1e6
if reactants == 'DD':
mhe3 = atomic_mass('He3')
Q = (md + md - mhe3 - mn)*ev_per_c2
E_n = mhe3/(mhe3 + mn)*Q
w0 = 82.542

# Low-T constants for peak shift (Table III)
a1 = 4.69515
a2 = -0.040729
a3 = 0.47
a4 = 0.81844

# Low-T constants for width correction (Table III)
b1 = 1.7013e-3
b2 = 0.16888
b3 = 0.49
b4 = 7.9460e-4

# High-T constants for peak shift (Table IV)
a5 = 18.225
a6 = 2.1525

# High-T constants for width correction (Table IV)
b5 = 8.4619e-3
b6 = 8.3241e-4

elif reactants == 'DT':
mt = atomic_mass('H3')
ma = atomic_mass('He4')
Q = (md + mt - ma - mn)*ev_per_c2
E_n = ma/(ma + mn)*Q
w0 = 177.259

# Low-T constants for peak shift (Table III)
a1 = 5.30509
a2 = 2.4736e-3
a3 = 1.84
a4 = 1.3818

# Low-T constants for width correction (Table III)
b1 = 5.1068e-4
b2 = 7.6223e-3
b3 = 1.78
b4 = 8.7691e-5

# High-T constants for peak shift (Table IV)
a5 = 37.771
a6 = 0.92181

# High-T constants for width correction (Table IV)
b5 = 2.0199e-3
b6 = 5.9501e-5
else:
raise ValueError("Invalid reactants specified. Must be 'DD' or 'DT'.")

# Ion temperature in keV
T = ion_temp * 1e-3

if T <= 40.0:
# Low-temperature interpolation (Table III, 0 < T_i <= 40 keV)
Delta_E = a1/(1 + a2*T**a3)*T**(2/3) + a4*T
delta_w = b1/(1 + b2*T**b3)*T**(2/3) + b4*T
else:
# High-temperature interpolation (Table IV, 40 < T_i < 100 keV)
Delta_E = a5 + a6*T
delta_w = b5 + b6*T

# Calculate FWHM
fwhm = (w0*(1 + delta_w) * sqrt(T))*1e3

sigma = fwhm / (2*sqrt(2*log(2)))
return Normal(E_n + Delta_E * 1e3, sigma, bias=bias)


class Tabular(Univariate):
"""Piecewise continuous probability distribution.

Expand Down
83 changes: 83 additions & 0 deletions tests/unit_tests/test_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,3 +930,86 @@ def test_reference_vwu_normalization():

# reference_v should be unit length
assert np.isclose(np.linalg.norm(reference_v), 1.0, atol=1e-12)


def test_fusion_spectrum_dd():
d = openmc.stats.fusion_neutron_spectrum(10e3, 'DD')
assert isinstance(d, openmc.stats.Normal)

# E_0 for D(d,n)3He is ~2.45 MeV; thermal shift at 10 keV should be
# several tens of keV, so mean should be noticeably above E_0
assert d.mean_value > 2.45e6
assert d.mean_value < 2.6e6

# Standard deviation should be positive and on order of ~50-100 keV
assert d.std_dev > 30e3
assert d.std_dev < 200e3


def test_fusion_spectrum_dt():
d = openmc.stats.fusion_neutron_spectrum(10e3, 'DT')
assert isinstance(d, openmc.stats.Normal)

# E_0 for T(d,n)alpha is ~14.02 MeV; with thermal shift mean should be
# above E_0 by several tens of keV
assert d.mean_value > 14.02e6
assert d.mean_value < 14.2e6

# Standard deviation should be on order of ~200-400 keV
assert d.std_dev > 100e3
assert d.std_dev < 500e3


def test_fusion_spectrum_temp_continuity():
# Verify the low-T and high-T formulas produce nearly identical results
# at the 40 keV switchover point
d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DT')
d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DT')

assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3)
assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3)

# Same check for DD
d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DD')
d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DD')

assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3)
assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3)


def test_fusion_spectrum_high_temp():
# At T_i = 80 keV (high-T regime), ensure the function still produces
# reasonable results using Table IV formulas
for reactants in ('DD', 'DT'):
d = openmc.stats.fusion_neutron_spectrum(80e3, reactants)
assert isinstance(d, openmc.stats.Normal)
assert d.mean_value > 0
assert d.std_dev > 0

# DT mean at 80 keV should be higher than at 10 keV
d_10 = openmc.stats.fusion_neutron_spectrum(10e3, 'DT')
d_80 = openmc.stats.fusion_neutron_spectrum(80e3, 'DT')
assert d_80.mean_value > d_10.mean_value
assert d_80.std_dev > d_10.std_dev


def test_fusion_spectrum_zero_temp():
# At very low temperature, mean should approach E_0 and width should
# approach zero
d = openmc.stats.fusion_neutron_spectrum(1.0, 'DT')
assert d.mean_value == pytest.approx(14.049e6, rel=1e-3)
assert d.std_dev < 5e3 # width approaches zero at low temperature


def test_fusion_spectrum_invalid():
# Invalid reactant string should raise an error
with pytest.raises(ValueError):
openmc.stats.fusion_neutron_spectrum(10e3, '🐔🧇')

# Negative temperature should raise an error
with pytest.raises(ValueError):
openmc.stats.fusion_neutron_spectrum(-10e3, 'DT')

# Temperature above 100 keV should raise an error
with pytest.raises(ValueError):
openmc.stats.fusion_neutron_spectrum(101e3, 'DT')
Loading