Approximate multigroup velocity (#3766)

Co-authored-by: Adam Nelson <1037107+nelsonag@users.noreply.github.com>
Co-authored-by: Paul Romano <paul.k.romano@gmail.com>
This commit is contained in:
GuySten 2026-04-02 19:59:02 +03:00 committed by GitHub
parent 97d9a839c2
commit d9b30bbbd5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 145 additions and 13 deletions

View file

@ -133,6 +133,10 @@ Temperature-dependent data, provided for temperature <TTT>K.
This dataset is optional. This is a 1-D vector if `representation`
is "isotropic", or a 3-D vector if `representation` is "angle"
with dimensions of [polar][azimuthal][groups].
When this data is not available, an approximation using the
group energy boundaries is used. For more information see
the particle speed subsection in the multigroup-data section
of the theory manual.
**/<library name>/<TTT>K/scatter_data/**

View file

@ -289,6 +289,48 @@ sections. This allows flexibility for the model to use highly anisotropic
scattering information in the water while the fuel can be simulated with linear
or even isotropic scattering.
Particle Speed
--------------
When using a multigroup representation of cross sections, the particle speed has
meaning only in an average sense. The particle speed is important when modeling
dynamic behavior. OpenMC calculates the particle speed using the inverse
velocity multigroup data if it is available. If such data is not available,
OpenMC uses an approximate velocity using the group energy bounds in the
following way:
.. math::
\frac{1}{v_g} = \int_{E_{\text{min}}^g}^{E_{\text{max}}^g} \frac{1}{v(E)} \frac{\alpha}{E} dE
Where :math:`E_{\text{min}}^g` and :math:`E_{\text{max}}^g` are the group energy
boundaries for group :math:`g`. :math:`v(E)` is the neutron velocity calculated
using relativistic kinematics, :math:`\alpha` is a normalization constant for the
:math:`\frac{1}{E}` spectrum.
This equation is valid when inside the group boundaries the neutron spectrum
follows a typical :math:`\frac{1}{E}` slowing down spectrum. This assumption is
widely used when generating fine group neutron cross section data libraries from
continuous energy data.
The solution to this equation is:
.. math::
\frac{1}{v_g} = \frac{1}{c \log\left(\frac{E_{\text{max}}^g}{E_{\text{min}}^g}\right)}
\left[ 2(\operatorname{arctanh}(k_{\text{max}}^{-1}) - \operatorname{arctanh}(k_{\text{min}}^{-1}))
- (k_{\text{max}}-k_{\text{min}}) \right]
where :math:`c` is the speed of light and :math:`k_{\text{max}}`,
:math:`k_{\text{min}}` are defined by a change of variables:
.. math::
k = \sqrt{1+\frac{2 m_n c^2}{E}}
where :math:`E` is the particle kinetic energy and :math:`m_n` is the neutron
rest mass.
.. _logarithmic mapping technique:
https://mcnp.lanl.gov/pdf_files/TechReport_2014_LANL_LA-UR-14-24530_Brown.pdf
.. _Hwang: https://doi.org/10.13182/NSE87-A16381

View file

@ -61,6 +61,8 @@ public:
vector<double> energy_bin_avg_;
vector<double> rev_energy_bins_;
vector<vector<double>> nuc_temps_; // all available temperatures
vector<double>
default_inverse_velocity_; // approximate default inverse-velocity data
};
namespace data {

View file

@ -39,6 +39,8 @@ public:
double speed() const;
double mass() const;
//! create a secondary particle
//
//! stores the current phase space attributes of the particle in the

View file

@ -78,6 +78,7 @@ class EnergyGroups:
@group_edges.setter
def group_edges(self, edges):
cv.check_type('group edges', edges, Iterable, Real)
cv.check_increasing('group edges', edges)
cv.check_greater_than('number of group edges', len(edges), 1)
self._group_edges = np.array(edges)

View file

@ -510,6 +510,8 @@ double Mgxs::get_xs(MgxsType xstype, int gin, const int* gout, const double* mu,
break;
case MgxsType::INVERSE_VELOCITY:
val = xs_t->inverse_velocity(a, gin);
if (!(val > 0))
val = data::mg.default_inverse_velocity_[gin];
break;
case MgxsType::DECAY_RATE:
if (dg != nullptr) {

View file

@ -237,6 +237,19 @@ void MgxsInterface::read_header(const std::string& path_cross_sections)
"library file!");
}
// Calculate approximate default inverse velocity data
for (int i = 0; i < energy_bins_.size() - 1; ++i) {
double e_min = std::max(energy_bins_[i + 1], 1e-5);
double e_max = energy_bins_[i];
double alpha = 1.0 / (C_LIGHT * std::log(e_max / e_min));
double k_max = std::sqrt(1 + 2.0 * MASS_NEUTRON_EV / e_max);
double k_min = std::sqrt(1 + 2.0 * MASS_NEUTRON_EV / e_min);
double inv_v =
alpha * (2.0 * (std::atanh(1.0 / k_max) - std::atanh(1.0 / k_min)) -
(k_max - k_min));
default_inverse_velocity_.push_back(inv_v);
}
// Close MGXS HDF5 file
file_close(file_id);
}

View file

@ -48,26 +48,33 @@ double Particle::speed() const
{
if (settings::run_CE) {
// Determine mass in eV/c^2
double mass;
switch (type().pdg_number()) {
case PDG_NEUTRON:
mass = MASS_NEUTRON_EV;
case PDG_ELECTRON:
case PDG_POSITRON:
mass = MASS_ELECTRON_EV;
default:
mass = this->type().mass() * AMU_EV;
}
double mass = this->mass();
// Equivalent to C * sqrt(1-(m/(m+E))^2) without problem at E<<m:
return C_LIGHT * std::sqrt(this->E() * (this->E() + 2 * mass)) /
(this->E() + mass);
} else {
auto& macro_xs = data::mg.macro_xs_[this->material()];
auto mat = this->material();
if (mat == MATERIAL_VOID)
return 1.0 / data::mg.default_inverse_velocity_[this->g()];
auto& macro_xs = data::mg.macro_xs_[mat];
int macro_t = this->mg_xs_cache().t;
int macro_a = macro_xs.get_angle_index(this->u());
return 1.0 / macro_xs.get_xs(MgxsType::INVERSE_VELOCITY, this->g(), nullptr,
nullptr, nullptr, macro_t, macro_a);
return 1.0 / macro_xs.get_xs(
MgxsType::INVERSE_VELOCITY, this->g(), macro_t, macro_a);
}
}
double Particle::mass() const
{
switch (type().pdg_number()) {
case PDG_NEUTRON:
return MASS_NEUTRON_EV;
case PDG_ELECTRON:
case PDG_POSITRON:
return MASS_ELECTRON_EV;
default:
return this->type().mass() * AMU_EV;
}
}

View file

@ -0,0 +1,59 @@
import openmc
import numpy as np
import pytest
@pytest.fixture
def one_group_lib():
groups = openmc.mgxs.EnergyGroups([0.0, 20.0e6])
xsdata = openmc.XSdata('slab_mat', groups)
xsdata.order = 0
xsdata.set_total([0.0])
xsdata.set_absorption([0.0])
xsdata.set_scatter_matrix([[[0.0]]])
mg_library = openmc.MGXSLibrary(groups)
mg_library.add_xsdata(xsdata)
name = 'mgxs.h5'
mg_library.export_to_hdf5(name)
yield name
@pytest.fixture
def slab_model(one_group_lib):
model = openmc.Model()
mat = openmc.Material(name='slab_material')
mat.set_density('macro', 1.0)
mat.add_macroscopic('slab_mat')
model.materials = openmc.Materials([mat])
model.materials.cross_sections = one_group_lib
x_min = openmc.XPlane(x0=0.0, boundary_type='vacuum')
x_max = openmc.XPlane(x0=10.0, boundary_type='vacuum')
y_min = openmc.YPlane(y0=-10.0, boundary_type='vacuum')
y_max = openmc.YPlane(y0=10.0, boundary_type='vacuum')
z_min = openmc.ZPlane(z0=-10.0, boundary_type='vacuum')
z_max = openmc.ZPlane(z0=19.0, boundary_type='vacuum')
cell = openmc.Cell(fill=mat, region=+z_min & -x_max & +y_min & -y_max & +z_min & -z_max)
model.geometry = openmc.Geometry([cell])
model.settings = openmc.Settings()
model.settings.energy_mode = 'multi-group'
model.settings.run_mode = 'fixed source'
model.settings.batches = 3
model.settings.particles = 10
source = openmc.IndependentSource()
source.space = openmc.stats.Point((5.0, 0.0, 0.0))
model.settings.source = source
return model
def test_inverse_velocity(run_in_tmpdir, slab_model):
tally = openmc.Tally()
tally.scores = ['flux','inverse-velocity']
slab_model.tallies = [tally]
slab_model.run(apply_tally_results=True)
inverse_velocity = tally.mean.squeeze()[1]/tally.mean.squeeze()[0]
assert inverse_velocity == pytest.approx(1.6144e-5, rel=1e-4)