Skip to content

Commit 3ce6cbf

Browse files
authored
Add fusion_neutron_spectrum to openmc.stats module (openmc-dev#3862)
1 parent 1578698 commit 3ce6cbf

File tree

3 files changed

+218
-2
lines changed

3 files changed

+218
-2
lines changed

docs/source/pythonapi/stats.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Univariate Probability Distributions
2929
:template: myfunction.rst
3030

3131
openmc.stats.delta_function
32+
openmc.stats.fusion_neutron_spectrum
3233
openmc.stats.muir
3334

3435
Angular Distributions

openmc/stats/univariate.py

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
from abc import ABC, abstractmethod
33
from collections import defaultdict
44
from collections.abc import Iterable, Sequence
5-
from copy import deepcopy
6-
from math import sqrt, pi, exp
5+
from math import sqrt, pi, exp, log
76
from numbers import Real
87
from warnings import warn
98

@@ -14,6 +13,7 @@
1413
import scipy
1514

1615
import openmc.checkvalue as cv
16+
from openmc.data import atomic_mass, NEUTRON_MASS
1717
from .._xml import get_elem_list, get_text
1818
from ..mixin import EqualityMixin
1919

@@ -1277,6 +1277,138 @@ def Muir(*args, **kwargs):
12771277
return muir(*args, **kwargs)
12781278

12791279

1280+
def fusion_neutron_spectrum(
1281+
ion_temp: float,
1282+
reactants: str = 'DD',
1283+
bias: Univariate | None = None
1284+
) -> Normal:
1285+
r"""Return a Gaussian energy distribution for fusion neutron emission.
1286+
1287+
Computes the mean energy and spectral width of the neutron energy spectrum
1288+
from thermonuclear fusion reactions in a plasma with Maxwellian ion velocity
1289+
distributions. The mean neutron energy is calculated as
1290+
1291+
.. math::
1292+
1293+
\langle E_n \rangle = E_0 + \Delta E_\text{th}
1294+
1295+
where :math:`E_0` is the neutron energy at zero ion temperature and
1296+
:math:`\Delta E_\text{th}` is the thermal peak shift due to the motion of
1297+
the reacting ions. The spectral width is characterized by the FWHM:
1298+
1299+
.. math::
1300+
1301+
W_{1/2} = \omega_0 (1 + \delta_\omega) \sqrt{T_i}
1302+
1303+
where :math:`\omega_0` is the width at the :math:`T_i \to 0` limit and
1304+
:math:`\delta_\omega` is a temperature-dependent correction term. Both
1305+
:math:`\Delta E_\text{th}` and :math:`\delta_\omega` are evaluated using
1306+
interpolation formulas from `Ballabio et al.
1307+
<https://doi.org/10.1088/0029-5515/38/11/310>`_: Table III for :math:`0 <
1308+
T_i \le 40` keV and Table IV for :math:`40 < T_i < 100` keV. The returned
1309+
distribution is a normal (Gaussian) approximation to the spectrum.
1310+
1311+
.. versionadded:: 0.15.4
1312+
1313+
Parameters
1314+
----------
1315+
ion_temp : float
1316+
Ion temperature of the plasma in [eV].
1317+
reactants : {'DD', 'DT'}
1318+
Fusion reactants. 'DD' corresponds to the D(d,n)\ :sup:`3`\ He reaction
1319+
and 'DT' to the T(d,n)\ :math:`\alpha` reaction.
1320+
bias : openmc.stats.Univariate, optional
1321+
Distribution for biased sampling.
1322+
1323+
Returns
1324+
-------
1325+
openmc.stats.Normal
1326+
Normal distribution with mean and standard deviation corresponding to
1327+
the first and second moments of the fusion neutron energy spectrum. Both
1328+
the mean and standard deviation are in [eV].
1329+
1330+
"""
1331+
if ion_temp < 0.0 or ion_temp > 100e3:
1332+
raise ValueError("Ion temperature must be between 0 and 100 keV.")
1333+
1334+
# Formulas from doi:10.1088/0029-5515/38/11/310
1335+
mn = NEUTRON_MASS
1336+
md = atomic_mass('H2')
1337+
ev_per_c2 = 931.49410372*1e6
1338+
if reactants == 'DD':
1339+
mhe3 = atomic_mass('He3')
1340+
Q = (md + md - mhe3 - mn)*ev_per_c2
1341+
E_n = mhe3/(mhe3 + mn)*Q
1342+
w0 = 82.542
1343+
1344+
# Low-T constants for peak shift (Table III)
1345+
a1 = 4.69515
1346+
a2 = -0.040729
1347+
a3 = 0.47
1348+
a4 = 0.81844
1349+
1350+
# Low-T constants for width correction (Table III)
1351+
b1 = 1.7013e-3
1352+
b2 = 0.16888
1353+
b3 = 0.49
1354+
b4 = 7.9460e-4
1355+
1356+
# High-T constants for peak shift (Table IV)
1357+
a5 = 18.225
1358+
a6 = 2.1525
1359+
1360+
# High-T constants for width correction (Table IV)
1361+
b5 = 8.4619e-3
1362+
b6 = 8.3241e-4
1363+
1364+
elif reactants == 'DT':
1365+
mt = atomic_mass('H3')
1366+
ma = atomic_mass('He4')
1367+
Q = (md + mt - ma - mn)*ev_per_c2
1368+
E_n = ma/(ma + mn)*Q
1369+
w0 = 177.259
1370+
1371+
# Low-T constants for peak shift (Table III)
1372+
a1 = 5.30509
1373+
a2 = 2.4736e-3
1374+
a3 = 1.84
1375+
a4 = 1.3818
1376+
1377+
# Low-T constants for width correction (Table III)
1378+
b1 = 5.1068e-4
1379+
b2 = 7.6223e-3
1380+
b3 = 1.78
1381+
b4 = 8.7691e-5
1382+
1383+
# High-T constants for peak shift (Table IV)
1384+
a5 = 37.771
1385+
a6 = 0.92181
1386+
1387+
# High-T constants for width correction (Table IV)
1388+
b5 = 2.0199e-3
1389+
b6 = 5.9501e-5
1390+
else:
1391+
raise ValueError("Invalid reactants specified. Must be 'DD' or 'DT'.")
1392+
1393+
# Ion temperature in keV
1394+
T = ion_temp * 1e-3
1395+
1396+
if T <= 40.0:
1397+
# Low-temperature interpolation (Table III, 0 < T_i <= 40 keV)
1398+
Delta_E = a1/(1 + a2*T**a3)*T**(2/3) + a4*T
1399+
delta_w = b1/(1 + b2*T**b3)*T**(2/3) + b4*T
1400+
else:
1401+
# High-temperature interpolation (Table IV, 40 < T_i < 100 keV)
1402+
Delta_E = a5 + a6*T
1403+
delta_w = b5 + b6*T
1404+
1405+
# Calculate FWHM
1406+
fwhm = (w0*(1 + delta_w) * sqrt(T))*1e3
1407+
1408+
sigma = fwhm / (2*sqrt(2*log(2)))
1409+
return Normal(E_n + Delta_E * 1e3, sigma, bias=bias)
1410+
1411+
12801412
class Tabular(Univariate):
12811413
"""Piecewise continuous probability distribution.
12821414

tests/unit_tests/test_stats.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,3 +930,86 @@ def test_reference_vwu_normalization():
930930

931931
# reference_v should be unit length
932932
assert np.isclose(np.linalg.norm(reference_v), 1.0, atol=1e-12)
933+
934+
935+
def test_fusion_spectrum_dd():
936+
d = openmc.stats.fusion_neutron_spectrum(10e3, 'DD')
937+
assert isinstance(d, openmc.stats.Normal)
938+
939+
# E_0 for D(d,n)3He is ~2.45 MeV; thermal shift at 10 keV should be
940+
# several tens of keV, so mean should be noticeably above E_0
941+
assert d.mean_value > 2.45e6
942+
assert d.mean_value < 2.6e6
943+
944+
# Standard deviation should be positive and on order of ~50-100 keV
945+
assert d.std_dev > 30e3
946+
assert d.std_dev < 200e3
947+
948+
949+
def test_fusion_spectrum_dt():
950+
d = openmc.stats.fusion_neutron_spectrum(10e3, 'DT')
951+
assert isinstance(d, openmc.stats.Normal)
952+
953+
# E_0 for T(d,n)alpha is ~14.02 MeV; with thermal shift mean should be
954+
# above E_0 by several tens of keV
955+
assert d.mean_value > 14.02e6
956+
assert d.mean_value < 14.2e6
957+
958+
# Standard deviation should be on order of ~200-400 keV
959+
assert d.std_dev > 100e3
960+
assert d.std_dev < 500e3
961+
962+
963+
def test_fusion_spectrum_temp_continuity():
964+
# Verify the low-T and high-T formulas produce nearly identical results
965+
# at the 40 keV switchover point
966+
d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DT')
967+
d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DT')
968+
969+
assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3)
970+
assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3)
971+
972+
# Same check for DD
973+
d_lo = openmc.stats.fusion_neutron_spectrum(39.99e3, 'DD')
974+
d_hi = openmc.stats.fusion_neutron_spectrum(40.01e3, 'DD')
975+
976+
assert d_lo.mean_value == pytest.approx(d_hi.mean_value, rel=1e-3)
977+
assert d_lo.std_dev == pytest.approx(d_hi.std_dev, rel=1e-3)
978+
979+
980+
def test_fusion_spectrum_high_temp():
981+
# At T_i = 80 keV (high-T regime), ensure the function still produces
982+
# reasonable results using Table IV formulas
983+
for reactants in ('DD', 'DT'):
984+
d = openmc.stats.fusion_neutron_spectrum(80e3, reactants)
985+
assert isinstance(d, openmc.stats.Normal)
986+
assert d.mean_value > 0
987+
assert d.std_dev > 0
988+
989+
# DT mean at 80 keV should be higher than at 10 keV
990+
d_10 = openmc.stats.fusion_neutron_spectrum(10e3, 'DT')
991+
d_80 = openmc.stats.fusion_neutron_spectrum(80e3, 'DT')
992+
assert d_80.mean_value > d_10.mean_value
993+
assert d_80.std_dev > d_10.std_dev
994+
995+
996+
def test_fusion_spectrum_zero_temp():
997+
# At very low temperature, mean should approach E_0 and width should
998+
# approach zero
999+
d = openmc.stats.fusion_neutron_spectrum(1.0, 'DT')
1000+
assert d.mean_value == pytest.approx(14.049e6, rel=1e-3)
1001+
assert d.std_dev < 5e3 # width approaches zero at low temperature
1002+
1003+
1004+
def test_fusion_spectrum_invalid():
1005+
# Invalid reactant string should raise an error
1006+
with pytest.raises(ValueError):
1007+
openmc.stats.fusion_neutron_spectrum(10e3, '🐔🧇')
1008+
1009+
# Negative temperature should raise an error
1010+
with pytest.raises(ValueError):
1011+
openmc.stats.fusion_neutron_spectrum(-10e3, 'DT')
1012+
1013+
# Temperature above 100 keV should raise an error
1014+
with pytest.raises(ValueError):
1015+
openmc.stats.fusion_neutron_spectrum(101e3, 'DT')

0 commit comments

Comments
 (0)