OpenMC/openmc/data/multipole.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1302 lines
46 KiB
Python
Raw Permalink Normal View History

from numbers import Real
2017-05-22 17:14:25 -04:00
from math import exp, erf, pi, sqrt
2019-07-27 03:22:45 +00:00
from copy import deepcopy
2016-12-01 19:46:51 -05:00
import os
2016-12-01 17:18:28 -05:00
import h5py
2020-10-09 22:33:38 +08:00
import pickle
2016-12-01 17:18:28 -05:00
import numpy as np
from scipy.signal import find_peaks
2016-12-01 17:18:28 -05:00
2016-12-01 19:46:51 -05:00
import openmc.checkvalue as cv
from ..exceptions import DataError
from ..mixin import EqualityMixin
from . import WMP_VERSION, WMP_VERSION_MAJOR
from .data import K_BOLTZMANN
from .neutron import IncidentNeutron
from .resonance import ResonanceRange
from .vectfit import vectfit, evaluate
2019-07-25 17:10:09 +00:00
2016-12-01 17:18:28 -05:00
# Constants that determine which value to access
2016-12-05 14:25:41 -05:00
_MP_EA = 0 # Pole
2016-12-01 17:18:28 -05:00
# Residue indices
_MP_RS = 1 # Residue scattering
_MP_RA = 2 # Residue absorption
_MP_RF = 3 # Residue fission
2016-12-01 17:18:28 -05:00
# Polynomial fit indices
_FIT_S = 0 # Scattering
2016-12-05 14:25:41 -05:00
_FIT_A = 1 # Absorption
_FIT_F = 2 # Fission
2016-12-01 17:18:28 -05:00
2020-10-13 10:28:54 +08:00
# Upper temperature limit (K)
TEMPERATURE_LIMIT = 3000
2016-12-01 17:18:28 -05:00
2019-07-27 18:25:46 +00:00
# Logging control
2020-02-06 22:11:08 +08:00
DETAILED_LOGGING = 2
2019-07-27 18:25:46 +00:00
2020-10-09 22:33:38 +08:00
def _faddeeva(z):
2016-12-05 15:49:02 -05:00
r"""Evaluate the complex Faddeeva function.
2016-12-01 17:18:28 -05:00
Technically, the value we want is given by the equation:
2016-12-05 15:49:02 -05:00
.. math::
w(z) = \frac{i}{\pi} \int_{-\infty}^{\infty} \frac{1}{z - t}
\exp(-t^2) \text{d}t
2016-12-01 17:18:28 -05:00
as shown in Equation 63 from Hwang, R. N. "A rigorous pole
representation of multilevel cross sections and its practical
applications." Nuclear Science and Engineering 96.3 (1987): 192-209.
2016-12-05 15:49:02 -05:00
The :func:`scipy.special.wofz` function evaluates
:math:`w(z) = \exp(-z^2) \text{erfc}(-iz)`. These two forms of the Faddeeva
function are related by a transformation.
2016-12-01 17:18:28 -05:00
2016-12-05 15:49:02 -05:00
If we call the integral form :math:`w_\text{int}`, and the function form
:math:`w_\text{fun}`:
.. math::
w_\text{int}(z) =
\begin{cases}
w_\text{fun}(z) & \text{for } \text{Im}(z) > 0\\
-w_\text{fun}(z^*)^* & \text{for } \text{Im}(z) < 0
\end{cases}
2016-12-01 17:18:28 -05:00
Parameters
----------
2016-12-05 14:25:41 -05:00
z : complex
Argument to the Faddeeva function.
Returns
-------
2016-12-05 14:25:41 -05:00
complex
2016-12-05 15:49:02 -05:00
:math:`\frac{i}{\pi} \int_{-\infty}^{\infty} \frac{1}{z - t} \exp(-t^2)
\text{d}t`
2016-12-01 17:18:28 -05:00
"""
2016-12-05 15:49:02 -05:00
from scipy.special import wofz
2016-12-01 17:18:28 -05:00
if np.angle(z) > 0:
return wofz(z)
else:
2017-05-19 16:12:24 -04:00
return -np.conj(wofz(z.conjugate()))
2016-12-01 17:18:28 -05:00
def _broaden_wmp_polynomials(E, dopp, n):
2016-12-05 15:49:02 -05:00
r"""Evaluate Doppler-broadened windowed multipole curvefit.
2016-12-05 15:49:02 -05:00
The curvefit is a polynomial of the form :math:`\frac{a}{E}
+ \frac{b}{\sqrt{E}} + c + d \sqrt{E} + \ldots`
Parameters
----------
2019-07-19 15:36:30 -04:00
E : float
Energy to evaluate at.
2019-07-19 15:36:30 -04:00
dopp : float
sqrt(atomic weight ratio / kT) in units of eV.
2019-07-19 15:36:30 -04:00
n : int
Number of components to the polynomial.
Returns
-------
np.ndarray
The value of each Doppler-broadened curvefit polynomial term.
"""
2017-05-22 17:14:25 -04:00
sqrtE = sqrt(E)
2016-12-01 17:18:28 -05:00
beta = sqrtE * dopp
half_inv_dopp2 = 0.5 / dopp**2
quarter_inv_dopp4 = half_inv_dopp2**2
if beta > 6.0:
# Save time, ERF(6) is 1 to machine precision.
# beta/sqrtpi*exp(-beta**2) is also approximately 1 machine epsilon.
erf_beta = 1.0
2016-12-01 17:18:28 -05:00
exp_m_beta2 = 0.0
else:
erf_beta = erf(beta)
exp_m_beta2 = exp(-beta**2)
2016-12-01 17:18:28 -05:00
# Assume that, for sure, we'll use a second order (1/E, 1/V, const)
# fit, and no less.
factors = np.zeros(n)
factors[0] = erf_beta / E
2016-12-01 17:18:28 -05:00
factors[1] = 1.0 / sqrtE
factors[2] = (factors[0] * (half_inv_dopp2 + E)
2017-05-22 17:14:25 -04:00
+ exp_m_beta2 / (beta * sqrt(pi)))
2016-12-01 17:18:28 -05:00
2017-05-19 16:12:24 -04:00
# Perform recursive broadening of high order components. range(1, n-2)
# replaces a do i = 1, n-3. All indices are reduced by one due to the
# 1-based vs. 0-based indexing.
for i in range(1, n-2):
2016-12-01 17:18:28 -05:00
if i != 1:
factors[i+2] = (-factors[i-2] * (i - 1.0) * i * quarter_inv_dopp4
+ factors[i] * (E + (1.0 + 2.0 * i) * half_inv_dopp2))
2016-12-01 17:18:28 -05:00
else:
factors[i+2] = factors[i]*(E + (1.0 + 2.0 * i) * half_inv_dopp2)
2016-12-01 17:18:28 -05:00
return factors
2020-10-09 22:33:38 +08:00
def _vectfit_xs(energy, ce_xs, mts, rtol=1e-3, atol=1e-5, orders=None,
2020-10-09 22:33:38 +08:00
n_vf_iter=30, log=False, path_out=None):
"""Convert point-wise cross section to multipole data via vector fitting.
Parameters
----------
energy : np.ndarray
2019-01-02 09:28:41 -05:00
Energy array
2019-06-25 14:32:23 +00:00
ce_xs : np.ndarray
2020-10-09 22:33:38 +08:00
Point-wise cross sections to be fitted, with shape (number of reactions,
number of energy points)
2019-07-19 15:36:30 -04:00
mts : Iterable of int
2019-01-02 09:28:41 -05:00
Reaction list
2019-07-19 15:36:30 -04:00
rtol : float, optional
2019-01-02 09:28:41 -05:00
Relative error tolerance
2019-07-19 15:36:30 -04:00
atol : float, optional
2019-01-02 09:28:41 -05:00
Absolute error tolerance
2019-07-19 15:36:30 -04:00
orders : Iterable of int, optional
2019-01-02 09:28:41 -05:00
A list of orders (number of poles) to be searched
2019-07-26 14:37:56 +00:00
n_vf_iter : int, optional
2019-01-02 09:28:41 -05:00
Number of maximum VF iterations
2019-07-27 18:25:46 +00:00
log : bool or int, optional
2020-10-09 22:33:38 +08:00
Whether to print running logs (use int for verbosity control)
2019-01-02 09:28:41 -05:00
path_out : str, optional
2020-10-09 22:33:38 +08:00
Path to save the figures to show discrepancies between the original and
fitted cross sections for different reactions
Returns
-------
2020-10-09 22:33:38 +08:00
tuple
2019-01-02 09:28:41 -05:00
(poles, residues)
"""
ne = energy.size
nmt = len(mts)
if ce_xs.shape != (nmt, ne):
raise ValueError('Inconsistent cross section data.')
# construct test data: interpolate xs with finer grids
2020-10-09 22:33:38 +08:00
n_finer = 10
ne_test = (ne - 1)*n_finer + 1
test_energy = np.interp(np.arange(ne_test),
2020-10-09 22:33:38 +08:00
np.arange(ne_test, step=n_finer), energy)
2020-10-13 10:28:54 +08:00
test_energy[[0, -1]] = energy[[0, -1]] # avoid numerical issue
test_xs_ref = np.zeros((nmt, ne_test))
for i in range(nmt):
test_xs_ref[i] = np.interp(test_energy, energy, ce_xs[i])
if log:
print(f"\tenergy: {energy[0]:.3e} to {energy[-1]:.3e} eV ({ne} points)")
print(f"\terror tolerance: rtol={rtol}, atol={atol}")
2019-01-02 09:28:41 -05:00
2020-10-09 22:33:38 +08:00
# transform xs (sigma) and energy (E) to f (sigma*E) and s (sqrt(E)) to be
# compatible with the multipole representation
f = ce_xs * energy
s = np.sqrt(energy)
test_s = np.sqrt(test_energy)
2020-10-09 22:33:38 +08:00
# inverse weighting is used for minimizing the relative deviation instead of
# absolute deviation in vector fitting
with np.errstate(divide='ignore'):
weight = 1.0/f
2019-01-02 09:28:41 -05:00
2020-10-09 22:33:38 +08:00
# avoid too large weights which will harm the fitting accuracy
min_cross_section = 1e-7
for i in range(nmt):
2020-10-09 22:33:38 +08:00
if np.all(ce_xs[i] <= min_cross_section):
weight[i] = 1.0
2020-10-09 22:33:38 +08:00
elif np.any(ce_xs[i] <= min_cross_section):
weight[i, ce_xs[i] <= min_cross_section] = \
max(weight[i, ce_xs[i] > min_cross_section])
2019-07-19 15:36:30 -04:00
# detect peaks (resonances) and determine VF order search range
2020-10-09 22:33:38 +08:00
peaks, _ = find_peaks(ce_xs[0] + ce_xs[1])
n_peaks = peaks.size
if orders is not None:
# make sure orders are even integers
2020-10-09 22:33:38 +08:00
orders = list(set([int(i/2)*2 for i in orders if i >= 2]))
else:
lowest_order = max(2, 2*n_peaks)
highest_order = max(200, 4*n_peaks)
2020-10-09 22:33:38 +08:00
orders = list(range(lowest_order, highest_order + 1, 2))
if log:
print(f"Found {n_peaks} peaks")
print(f"Fitting orders from {orders[0]} to {orders[-1]}")
2019-07-19 15:36:30 -04:00
# perform VF with increasing orders
found_ideal = False
2020-10-13 10:28:54 +08:00
n_discarded = 0 # for accelation, number of discarded searches
best_quality = best_ratio = -np.inf
for i, order in enumerate(orders):
if log:
print(f"Order={order}({i}/{len(orders)})")
# initial guessed poles
2020-10-10 09:37:43 +08:00
poles_r = np.linspace(s[0], s[-1], order//2)
poles = poles_r + poles_r*0.01j
poles = np.sort(np.append(poles, np.conj(poles)))
found_better = False
# fitting iteration
2019-01-02 09:28:41 -05:00
for i_vf in range(n_vf_iter):
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
print(f"VF iteration {i_vf + 1}/{n_vf_iter}")
# call vf
poles, residues, *_ = vectfit(f, s, poles, weight)
# convert real pole to conjugate pairs
n_real_poles = 0
new_poles = []
for p in poles:
p_r, p_i = np.real(p), np.imag(p)
if (s[0] <= p_r <= s[-1]) and p_i == 0.:
new_poles += [p_r+p_r*0.01j, p_r-p_r*0.01j]
n_real_poles += 1
else:
new_poles += [p]
new_poles = np.array(new_poles)
# re-calculate residues if poles changed
if n_real_poles > 0:
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
print(f" # real poles: {n_real_poles}")
new_poles, residues, *_ = \
vectfit(f, s, new_poles, weight, skip_pole_update=True)
# assess the result on test grid
test_xs = evaluate(test_s, new_poles, residues) / test_energy
abserr = np.abs(test_xs - test_xs_ref)
with np.errstate(invalid='ignore', divide='ignore'):
2020-10-13 10:28:54 +08:00
relerr = abserr / test_xs_ref
if np.any(np.isnan(abserr)):
maxre, ratio, ratio2 = np.inf, -np.inf, -np.inf
elif np.all(abserr <= atol):
maxre, ratio, ratio2 = 0., 1., 1.
else:
maxre = np.max(relerr[abserr > atol])
ratio = np.sum((relerr < rtol) | (abserr < atol)) / relerr.size
ratio2 = np.sum((relerr < 10*rtol) | (abserr < atol)) / relerr.size
2020-10-09 22:33:38 +08:00
# define a metric for choosing the best fitting results
# basically, it is preferred to have more points within accuracy
# tolerance, smaller maximum deviation and fewer poles
#TODO: improve the metric with clearer basis
2019-01-02 09:28:41 -05:00
quality = ratio + ratio2 - min(0.1*maxre, 1) - 0.001*new_poles.size
if np.any(test_xs < -atol):
quality = -np.inf
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
print(f" # poles: {new_poles.size}")
print(f" Max relative error: {maxre * 100:.3f}%")
print(f" Satisfaction: {ratio * 100:.1f}%, {ratio2 * 100:.1f}%")
print(f" Quality: {quality:.2f}")
if quality > best_quality:
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
2020-10-13 10:28:54 +08:00
print(" Best so far!")
found_better = True
best_quality, best_ratio = quality, ratio
best_poles, best_residues = new_poles, residues
best_test_xs, best_relerr = test_xs, relerr
if best_ratio >= 1.0:
if log:
print("Found ideal results. Stop!")
found_ideal = True
break
else:
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
print(" Discarded!")
if found_ideal:
break
# acceleration
if found_better:
n_discarded = 0
else:
if order > max(2*n_peaks, 50) and best_ratio > 0.7:
n_discarded += 1
if n_discarded >= 10 or (n_discarded >= 5 and best_ratio > 0.9):
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
print("Couldn't get better results. Stop!")
break
# merge conjugate poles
real_idx = []
conj_idx = []
found_conj = False
for i, p in enumerate(best_poles):
if found_conj:
found_conj = False
continue
if np.imag(p) == 0.:
real_idx.append(i)
else:
2020-10-09 22:33:38 +08:00
if i < best_poles.size and np.conj(p) == best_poles[i + 1]:
found_conj = True
conj_idx.append(i)
else:
raise RuntimeError("Complex poles are not conjugate!")
if log:
print("Found {} real poles and {} conjugate complex pairs.".format(
len(real_idx), len(conj_idx)))
2020-10-09 22:58:02 +08:00
mp_poles = best_poles[real_idx + conj_idx]
mp_residues = np.concatenate((best_residues[:, real_idx],
best_residues[:, conj_idx]*2), axis=1)/1j
if log:
print(f"Final number of poles: {mp_poles.size}")
2019-01-02 09:28:41 -05:00
if path_out:
if not os.path.exists(path_out):
os.makedirs(path_out)
for i, mt in enumerate(mts):
if not test_xs_ref[i].any():
continue
import matplotlib.pyplot as plt
fig, ax1 = plt.subplots()
lns1 = ax1.loglog(test_energy, test_xs_ref[i], 'g', label="ACE xs")
lns2 = ax1.loglog(test_energy, best_test_xs[i], 'b', label="VF xs")
ax2 = ax1.twinx()
lns3 = ax2.loglog(test_energy, best_relerr[i], 'r',
label="Relative error", alpha=0.5)
lns = lns1 + lns2 + lns3
labels = [l.get_label() for l in lns]
ax1.legend(lns, labels, loc='best')
ax1.set_xlabel('energy (eV)')
ax1.set_ylabel('cross section (b)', color='b')
ax1.tick_params('y', colors='b')
ax2.set_ylabel('relative error', color='r')
ax2.tick_params('y', colors='r')
plt.title(f"MT {mt} vector fitted with {mp_poles.size} poles")
fig.tight_layout()
2019-01-02 09:28:41 -05:00
fig_file = os.path.join(path_out, "{:.0f}-{:.0f}_MT{}.png".format(
energy[0], energy[-1], mt))
plt.savefig(fig_file)
plt.close()
if log:
print(f"Saved figure: {fig_file}")
return (mp_poles, mp_residues)
2016-12-01 17:18:28 -05:00
def vectfit_nuclide(endf_file, njoy_error=5e-4, vf_pieces=None,
log=False, path_out=None, mp_filename=None,
**kwargs):
2019-07-19 15:36:30 -04:00
r"""Generate multipole data for a nuclide from ENDF.
2019-01-02 09:28:41 -05:00
Parameters
----------
endf_file : str
Path to ENDF evaluation
njoy_error : float, optional
Fractional error tolerance for processing point-wise data with NJOY
vf_pieces : integer, optional
2020-10-09 22:33:38 +08:00
Number of equal-in-momentum spaced energy pieces for data fitting
2019-07-27 18:25:46 +00:00
log : bool or int, optional
2020-10-09 22:33:38 +08:00
Whether to print running logs (use int for verbosity control)
2019-01-02 09:28:41 -05:00
path_out : str, optional
2020-10-09 22:33:38 +08:00
Path to write out mutipole data file and vector fitting figures
2019-01-02 09:28:41 -05:00
mp_filename : str, optional
File name to write out multipole data
**kwargs
Keyword arguments passed to :func:`openmc.data.multipole._vectfit_xs`
Returns
-------
mp_data
2019-06-25 14:32:23 +00:00
Dictionary containing necessary multipole data of the nuclide
2019-01-02 09:28:41 -05:00
"""
# ======================================================================
# PREPARE POINT-WISE XS
# make 0K ACE data using njoy
if log:
print(f"Running NJOY to get 0K point-wise data (error={njoy_error})...")
2019-01-02 09:28:41 -05:00
nuc_ce = IncidentNeutron.from_njoy(endf_file, temperatures=[0.0],
error=njoy_error, broadr=False, heatr=False, purr=False)
if log:
print("Parsing cross sections within resolved resonance range...")
# Determine upper energy: the lower of RRR upper bound and first threshold
endf_res = IncidentNeutron.from_endf(endf_file).resonances
if hasattr(endf_res, 'resolved') and \
hasattr(endf_res.resolved, 'energy_max') and \
type(endf_res.resolved) is not ResonanceRange:
E_max = endf_res.resolved.energy_max
elif hasattr(endf_res, 'unresolved') and \
hasattr(endf_res.unresolved, 'energy_min'):
E_max = endf_res.unresolved.energy_min
else:
E_max = nuc_ce.energy['0K'][-1]
E_max_idx = np.searchsorted(nuc_ce.energy['0K'], E_max, side='right') - 1
for mt in nuc_ce.reactions:
if hasattr(nuc_ce.reactions[mt].xs['0K'], '_threshold_idx'):
threshold_idx = nuc_ce.reactions[mt].xs['0K']._threshold_idx
if 0 < threshold_idx < E_max_idx:
E_max_idx = threshold_idx
# parse energy and cross sections
2020-10-09 22:33:38 +08:00
energy = nuc_ce.energy['0K'][:E_max_idx + 1]
2019-01-02 09:28:41 -05:00
E_min, E_max = energy[0], energy[-1]
n_points = energy.size
total_xs = nuc_ce[1].xs['0K'](energy)
elastic_xs = nuc_ce[2].xs['0K'](energy)
2020-10-09 22:33:38 +08:00
2020-09-08 10:49:44 +08:00
try:
2019-01-02 09:28:41 -05:00
absorption_xs = nuc_ce[27].xs['0K'](energy)
2020-10-09 22:33:38 +08:00
except KeyError:
2020-09-08 10:49:44 +08:00
absorption_xs = np.zeros_like(total_xs)
2020-10-09 22:33:38 +08:00
2019-01-02 09:28:41 -05:00
fissionable = False
2020-09-08 10:49:44 +08:00
try:
2019-01-02 09:28:41 -05:00
fission_xs = nuc_ce[18].xs['0K'](energy)
fissionable = True
2020-10-09 22:33:38 +08:00
except KeyError:
pass
2019-01-02 09:28:41 -05:00
# make vectors
if fissionable:
ce_xs = np.vstack((elastic_xs, absorption_xs, fission_xs))
mts = [2, 27, 18]
else:
ce_xs = np.vstack((elastic_xs, absorption_xs))
mts = [2, 27]
if log:
print(f" MTs: {mts}")
print(f" Energy range: {E_min:.3e} to {E_max:.3e} eV ({n_points} points)")
2019-01-02 09:28:41 -05:00
# ======================================================================
# PERFORM VECTOR FITTING
if vf_pieces is None:
# divide into pieces for complex nuclides
peaks, _ = find_peaks(total_xs)
n_peaks = peaks.size
if n_peaks > 200 or n_points > 30000 or n_peaks * n_points > 100*10000:
vf_pieces = max(5, n_peaks // 50, n_points // 2000)
2019-01-02 09:28:41 -05:00
else:
vf_pieces = 1
piece_width = (sqrt(E_max) - sqrt(E_min)) / vf_pieces
alpha = nuc_ce.atomic_weight_ratio/(K_BOLTZMANN*TEMPERATURE_LIMIT)
2019-07-19 15:36:30 -04:00
poles, residues = [], []
2019-01-02 09:28:41 -05:00
# VF piece by piece
for i_piece in range(vf_pieces):
if log:
print(f"Vector fitting piece {i_piece + 1}/{vf_pieces}...")
2019-01-02 09:28:41 -05:00
# start E of this piece
e_bound = (sqrt(E_min) + piece_width*(i_piece-0.5))**2
if i_piece == 0 or sqrt(alpha*e_bound) < 4.0:
e_start = E_min
e_start_idx = 0
else:
2020-10-09 22:33:38 +08:00
e_start = max(E_min, (sqrt(alpha*e_bound) - 4.0)**2/alpha)
2019-01-02 09:28:41 -05:00
e_start_idx = np.searchsorted(energy, e_start, side='right') - 1
# end E of this piece
2020-10-09 22:33:38 +08:00
e_bound = (sqrt(E_min) + piece_width*(i_piece + 1))**2
2019-01-02 09:28:41 -05:00
e_end = min(E_max, (sqrt(alpha*e_bound) + 4.0)**2/alpha)
e_end_idx = np.searchsorted(energy, e_end, side='left') + 1
2020-10-09 22:33:38 +08:00
e_idx = range(e_start_idx, min(e_end_idx + 1, n_points))
2019-01-02 09:28:41 -05:00
2020-10-13 10:28:54 +08:00
p, r = _vectfit_xs(energy[e_idx], ce_xs[:, e_idx], mts, log=log,
path_out=path_out, **kwargs)
2019-01-02 09:28:41 -05:00
2019-07-19 15:36:30 -04:00
poles.append(p)
residues.append(r)
2019-01-02 09:28:41 -05:00
# collect multipole data into a dictionary
2019-01-02 09:28:41 -05:00
mp_data = {"name": nuc_ce.name,
"AWR": nuc_ce.atomic_weight_ratio,
"E_min": E_min,
"E_max": E_max,
"poles": poles,
2019-07-19 15:36:30 -04:00
"residues": residues}
2019-01-02 09:28:41 -05:00
2019-07-30 14:35:59 +00:00
# dump multipole data to file
2019-01-02 09:28:41 -05:00
if path_out:
if not os.path.exists(path_out):
os.makedirs(path_out)
if not mp_filename:
mp_filename = f"{nuc_ce.name}_mp.pickle"
2019-01-02 09:28:41 -05:00
mp_filename = os.path.join(path_out, mp_filename)
with open(mp_filename, 'wb') as f:
pickle.dump(mp_data, f)
if log:
print(f"Dumped multipole data to file: {mp_filename}")
2019-01-02 09:28:41 -05:00
return mp_data
2020-10-13 10:28:54 +08:00
2020-10-09 22:33:38 +08:00
def _windowing(mp_data, n_cf, rtol=1e-3, atol=1e-5, n_win=None, spacing=None,
log=False):
"""Generate windowed multipole library from multipole data with specific
2020-02-04 16:32:12 +08:00
settings of window size, curve fit order, etc.
2019-06-25 14:32:23 +00:00
Parameters
----------
mp_data : dict
Multipole data
2020-10-09 22:33:38 +08:00
n_cf : int
Curve fitting order
2019-07-19 15:36:30 -04:00
rtol : float, optional
2019-06-25 14:32:23 +00:00
Maximum relative error tolerance
2019-07-19 15:36:30 -04:00
atol : float, optional
2019-06-25 14:32:23 +00:00
Minimum absolute error tolerance
2019-07-19 15:36:30 -04:00
n_win : int, optional
2019-06-25 14:32:23 +00:00
Number of equal-in-mementum spaced energy windows
2019-07-22 11:31:39 -04:00
spacing : float, optional
Inner window spacing (sqrt energy space)
2019-07-27 18:25:46 +00:00
log : bool or int, optional
2020-10-09 22:33:38 +08:00
Whether to print running logs (use int for verbosity control)
2019-06-25 14:32:23 +00:00
Returns
-------
openmc.data.WindowedMultipole
Resonant cross sections represented in the windowed multipole
format.
2019-01-02 09:28:41 -05:00
"""
2019-06-25 14:32:23 +00:00
# unpack multipole data
name = mp_data["name"]
awr = mp_data["AWR"]
E_min = mp_data["E_min"]
E_max = mp_data["E_max"]
mp_poles = mp_data["poles"]
mp_residues = mp_data["residues"]
2019-07-19 15:36:30 -04:00
n_pieces = len(mp_poles)
piece_width = (sqrt(E_max) - sqrt(E_min)) / n_pieces
alpha = awr / (K_BOLTZMANN*TEMPERATURE_LIMIT)
2019-06-25 14:32:23 +00:00
2019-07-26 19:54:18 +00:00
# determine window size
2019-07-19 15:36:30 -04:00
if n_win is None:
2019-07-22 11:31:39 -04:00
if spacing is not None:
# ensure the windows are within the multipole energy range
2019-07-22 11:31:39 -04:00
n_win = int((sqrt(E_max) - sqrt(E_min)) / spacing)
E_max = (sqrt(E_min) + n_win*spacing)**2
else:
2019-07-26 19:54:18 +00:00
n_win = 1000
2019-07-19 15:36:30 -04:00
# inner window size
spacing = (sqrt(E_max) - sqrt(E_min)) / n_win
2019-07-22 11:31:39 -04:00
# make sure inner window size is smaller than energy piece size
2019-07-19 15:36:30 -04:00
if spacing > piece_width:
2019-07-22 11:31:39 -04:00
raise ValueError('Window spacing cannot be larger than piece spacing.')
2019-07-27 18:25:46 +00:00
if log:
print("Windowing:")
print(f" config: # windows={n_win}, spacing={spacing}, CF order={n_cf}")
print(f" error tolerance: rtol={rtol}, atol={atol}")
2019-07-27 18:25:46 +00:00
2019-07-19 15:36:30 -04:00
# sort poles (and residues) by the real component of the pole
for ip in range(n_pieces):
indices = mp_poles[ip].argsort()
mp_poles[ip] = mp_poles[ip][indices]
2019-07-25 17:10:09 +00:00
mp_residues[ip] = mp_residues[ip][:, indices]
2019-07-19 15:36:30 -04:00
2020-02-04 16:32:12 +08:00
# initialize an array to record whether each pole is used or not
2019-07-19 15:36:30 -04:00
poles_unused = [np.ones_like(p, dtype=int) for p in mp_poles]
# optimize the windows: the goal is to find the least set of significant
# consecutive poles and curve fit coefficients to reproduce cross section
win_data = []
for iw in range(n_win):
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
print(f"Processing window {iw + 1}/{n_win}...")
2019-07-22 11:31:39 -04:00
2019-07-19 15:36:30 -04:00
# inner window boundaries
inbegin = sqrt(E_min) + spacing * iw
inend = inbegin + spacing
incenter = (inbegin + inend) / 2.0
2020-09-03 21:19:46 +08:00
# extend window energy range for Doppler broadening
2019-07-19 15:36:30 -04:00
if iw == 0 or sqrt(alpha)*inbegin < 4.0:
e_start = inbegin**2
else:
2020-10-09 22:33:38 +08:00
e_start = max(E_min, (sqrt(alpha)*inbegin - 4.0)**2/alpha)
2019-07-19 15:36:30 -04:00
e_end = min(E_max, (sqrt(alpha)*inend + 4.0)**2/alpha)
# locate piece and relevant poles
2020-10-09 22:33:38 +08:00
i_piece = min(n_pieces - 1, int((inbegin - sqrt(E_min))/piece_width + 0.5))
2019-07-19 15:36:30 -04:00
poles, residues = mp_poles[i_piece], mp_residues[i_piece]
n_poles = poles.size
# generate energy points for fitting: equally spaced in momentum
n_points = min(max(100, int((e_end - e_start)*4)), 10000)
energy_sqrt = np.linspace(np.sqrt(e_start), np.sqrt(e_end), n_points)
energy = energy_sqrt**2
2020-02-04 16:32:12 +08:00
2020-10-12 21:16:54 +08:00
# reference xs from multipole form, note the residue terms in the
# multipole and vector fitting representations differ by a 1j
xs_ref = evaluate(energy_sqrt, poles, residues*1j) / energy
2019-07-19 15:36:30 -04:00
2019-07-25 17:10:09 +00:00
# curve fit matrix
2020-10-09 22:33:38 +08:00
matrix = np.vstack([energy**(0.5*i - 1) for i in range(n_cf + 1)]).T
2019-07-25 17:10:09 +00:00
2019-07-19 15:36:30 -04:00
# start from 0 poles, initialize pointers to the center nearest pole
center_pole_ind = np.argmin((np.fabs(poles.real - incenter)))
2020-10-09 22:33:38 +08:00
lp = rp = center_pole_ind
2019-07-19 15:36:30 -04:00
while True:
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
print(f"Trying poles {lp} to {rp}")
2020-02-04 16:32:12 +08:00
2019-07-19 15:36:30 -04:00
# calculate the cross sections contributed by the windowed poles
if rp > lp:
xs_wp = evaluate(energy_sqrt, poles[lp:rp],
2019-07-30 14:35:59 +00:00
residues[:, lp:rp]*1j) / energy
2019-07-19 15:36:30 -04:00
else:
xs_wp = np.zeros_like(xs_ref)
2019-07-25 17:10:09 +00:00
# do least square curve fit on the remains
coefs = np.linalg.lstsq(matrix, (xs_ref - xs_wp).T, rcond=None)[0]
xs_fit = (matrix @ coefs).T
2019-07-19 15:36:30 -04:00
# assess the result
abserr = np.abs(xs_fit + xs_wp - xs_ref)
with np.errstate(invalid='ignore', divide='ignore'):
2020-10-13 10:28:54 +08:00
relerr = abserr / xs_ref
2019-07-19 15:36:30 -04:00
if not np.any(np.isnan(abserr)):
2020-10-09 22:33:38 +08:00
re = relerr[abserr > atol]
2019-07-27 03:22:45 +00:00
if re.size == 0 or np.all(re <= rtol) or \
2020-10-09 22:33:38 +08:00
(re.max() <= 2*rtol and (re > rtol).sum() <= 0.01*relerr.size) or \
(iw == 0 and np.all(relerr.mean(axis=1) <= rtol)):
2019-07-19 15:36:30 -04:00
# meet tolerances
2020-02-06 22:11:08 +08:00
if log >= DETAILED_LOGGING:
2019-07-25 17:10:09 +00:00
print("Accuracy satisfied.")
2019-07-19 15:36:30 -04:00
break
2019-06-25 14:32:23 +00:00
2020-02-04 16:32:12 +08:00
# we expect pure curvefit will succeed for the first window
2019-07-27 18:25:46 +00:00
# TODO: find the energy boundary below which no poles are allowed
2019-07-27 03:22:45 +00:00
if iw == 0:
2019-07-27 18:25:46 +00:00
raise RuntimeError('Pure curvefit failed for the first window!')
2019-07-27 03:22:45 +00:00
2020-02-04 16:32:12 +08:00
# try to include one more pole (next center nearest)
2019-07-25 17:10:09 +00:00
if rp >= n_poles:
2020-10-09 22:33:38 +08:00
lp -= 1
elif lp <= 0 or poles[rp] - incenter <= incenter - poles[lp - 1]:
rp += 1
2019-07-19 15:36:30 -04:00
else:
2020-10-09 22:33:38 +08:00
lp -= 1
2019-07-19 15:36:30 -04:00
# save data for this window
win_data.append((i_piece, lp, rp, coefs))
2020-02-04 16:32:12 +08:00
2019-07-22 11:31:39 -04:00
# mark the windowed poles as used poles
2019-07-19 15:36:30 -04:00
poles_unused[i_piece][lp:rp] = 0
2019-07-30 14:35:59 +00:00
# flatten and shrink by removing unused poles
2020-10-13 10:28:54 +08:00
data = [] # used poles and residues
2019-07-19 15:36:30 -04:00
for ip in range(n_pieces):
used = (poles_unused[ip] == 0)
2019-07-25 17:10:09 +00:00
# stack poles and residues for library format
data.append(np.vstack([mp_poles[ip][used], mp_residues[ip][:, used]]).T)
2019-07-19 15:36:30 -04:00
# stack poles/residues in sequence vertically
data = np.vstack(data)
# new start/end pole indices
windows = []
curvefit = []
for iw in range(n_win):
ip, lp, rp, coefs = win_data[iw]
2019-07-25 17:10:09 +00:00
# adjust indices and change to 1-based for the library format
n_prev_poles = sum([poles_unused[i].size for i in range(ip)])
2020-10-09 22:33:38 +08:00
n_unused = sum([(poles_unused[i] == 1).sum() for i in range(ip)]) + \
(poles_unused[ip][:lp] == 1).sum()
lp += n_prev_poles - n_unused + 1
rp += n_prev_poles - n_unused
2019-07-19 15:36:30 -04:00
windows.append([lp, rp])
curvefit.append(coefs)
# construct the WindowedMultipole object
wmp = WindowedMultipole(name)
wmp.spacing = spacing
wmp.sqrtAWR = sqrt(awr)
wmp.E_min = E_min
wmp.E_max = E_max
wmp.data = data
wmp.windows = np.asarray(windows)
wmp.curvefit = np.asarray(curvefit)
2019-07-22 11:31:39 -04:00
# TODO: check if Doppler brodening of the polynomial curvefit is negligible
2019-07-19 15:36:30 -04:00
wmp.broaden_poly = np.ones((n_win,), dtype=bool)
return wmp
2019-01-02 09:28:41 -05:00
2020-10-13 10:28:54 +08:00
2016-12-01 17:18:28 -05:00
class WindowedMultipole(EqualityMixin):
"""Resonant cross sections represented in the windowed multipole format.
Parameters
----------
2018-10-19 16:48:12 -04:00
name : str
Name of the nuclide using the GNDS naming convention
Attributes
----------
2019-07-26 20:18:12 +00:00
name : str
Name of the nuclide using the GNDS naming convention
2019-07-26 19:54:18 +00:00
spacing : float
The width of each window in sqrt(E)-space. For example, the frst window
will end at (sqrt(E_min) + spacing)**2 and the second window at
(sqrt(E_min) + 2*spacing)**2.
2019-07-26 19:54:18 +00:00
sqrtAWR : float
Square root of the atomic weight ratio of the target nuclide.
2019-07-26 19:54:18 +00:00
E_min : float
Lowest energy in eV the library is valid for.
2019-07-26 19:54:18 +00:00
E_max : float
Highest energy in eV the library is valid for.
data : np.ndarray
A 2D array of complex poles and residues. data[i, 0] gives the energy
at which pole i is located. data[i, 1:] gives the residues associated
with the i-th pole. There are 3 residues, one each for the scattering,
absorption, and fission channels.
windows : np.ndarray
A 2D array of Integral values. windows[i, 0] - 1 is the index of the
first pole in window i. windows[i, 1] - 1 is the index of the last pole
in window i.
broaden_poly : np.ndarray
A 1D array of boolean values indicating whether or not the polynomial
curvefit in that window should be Doppler broadened.
curvefit : np.ndarray
A 3D array of Real curvefit polynomial coefficients. curvefit[i, 0, :]
gives coefficients for the scattering cross section in window i.
curvefit[i, 1, :] gives absorption coefficients and curvefit[i, 2, :]
gives fission coefficients. The polynomial terms are increasing powers
of sqrt(E) starting with 1/E e.g:
a/E + b/sqrt(E) + c + d sqrt(E) + ...
"""
2018-10-19 16:48:12 -04:00
def __init__(self, name):
self.name = name
2016-12-01 19:46:51 -05:00
self.spacing = None
self.sqrtAWR = None
self.E_min = None
self.E_max = None
2016-12-01 19:46:51 -05:00
self.data = None
self.windows = None
2016-12-01 19:46:51 -05:00
self.broaden_poly = None
self.curvefit = None
2018-10-19 16:48:12 -04:00
@property
def name(self):
return self._name
@name.setter
def name(self, name):
cv.check_type('name', name, str)
self._name = name
2016-12-01 19:46:51 -05:00
@property
def fit_order(self):
return self.curvefit.shape[1] - 1
2016-12-01 19:46:51 -05:00
@property
def fissionable(self):
return self.data.shape[1] == 4
2016-12-01 19:46:51 -05:00
2019-07-26 20:18:12 +00:00
@property
def n_poles(self):
return self.data.shape[0]
@property
def n_windows(self):
return self.windows.shape[0]
@property
def poles_per_window(self):
return (self.windows[:, 1] - self.windows[:, 0] + 1).mean()
2016-12-01 19:46:51 -05:00
@property
def spacing(self):
return self._spacing
@spacing.setter
def spacing(self, spacing):
if spacing is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('spacing', spacing, Real)
cv.check_greater_than('spacing', spacing, 0.0, equality=False)
2016-12-01 19:46:51 -05:00
self._spacing = spacing
@property
def sqrtAWR(self):
return self._sqrtAWR
2016-12-01 19:46:51 -05:00
@sqrtAWR.setter
def sqrtAWR(self, sqrtAWR):
if sqrtAWR is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('sqrtAWR', sqrtAWR, Real)
cv.check_greater_than('sqrtAWR', sqrtAWR, 0.0, equality=False)
2016-12-01 19:46:51 -05:00
self._sqrtAWR = sqrtAWR
@property
def E_min(self):
return self._E_min
@E_min.setter
def E_min(self, E_min):
if E_min is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('E_min', E_min, Real)
cv.check_greater_than('E_min', E_min, 0.0, equality=True)
self._E_min = E_min
2016-12-01 19:46:51 -05:00
@property
def E_max(self):
return self._E_max
@E_max.setter
def E_max(self, E_max):
if E_max is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('E_max', E_max, Real)
cv.check_greater_than('E_max', E_max, 0.0, equality=False)
self._E_max = E_max
2016-12-01 19:46:51 -05:00
@property
def data(self):
return self._data
2016-12-01 19:46:51 -05:00
@data.setter
def data(self, data):
if data is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('data', data, np.ndarray)
2016-12-01 19:46:51 -05:00
if len(data.shape) != 2:
raise ValueError('Multipole data arrays must be 2D')
if data.shape[1] not in (3, 4):
raise ValueError(
'data.shape[1] must be 3 or 4. One value for the pole.'
' One each for the scattering and absorption residues. '
'Possibly one more for a fission residue.')
2018-08-18 21:25:06 -04:00
if not np.issubdtype(data.dtype, np.complexfloating):
2016-12-01 19:46:51 -05:00
raise TypeError('Multipole data arrays must be complex dtype')
self._data = data
@property
def windows(self):
return self._windows
@windows.setter
def windows(self, windows):
if windows is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('windows', windows, np.ndarray)
if len(windows.shape) != 2:
raise ValueError('Multipole windows arrays must be 2D')
2018-08-18 21:25:06 -04:00
if not np.issubdtype(windows.dtype, np.integer):
raise TypeError('Multipole windows arrays must be integer'
2016-12-01 19:46:51 -05:00
' dtype')
self._windows = windows
2016-12-01 19:46:51 -05:00
@property
def broaden_poly(self):
return self._broaden_poly
2016-12-01 19:46:51 -05:00
@broaden_poly.setter
def broaden_poly(self, broaden_poly):
if broaden_poly is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('broaden_poly', broaden_poly, np.ndarray)
2016-12-01 19:46:51 -05:00
if len(broaden_poly.shape) != 1:
raise ValueError('Multipole broaden_poly arrays must be 1D')
2018-08-18 21:25:06 -04:00
if not np.issubdtype(broaden_poly.dtype, np.bool_):
2016-12-01 19:46:51 -05:00
raise TypeError('Multipole broaden_poly arrays must be boolean'
' dtype')
self._broaden_poly = broaden_poly
@property
def curvefit(self):
return self._curvefit
2016-12-01 19:46:51 -05:00
@curvefit.setter
def curvefit(self, curvefit):
if curvefit is not None:
2018-08-17 14:22:25 -04:00
cv.check_type('curvefit', curvefit, np.ndarray)
2016-12-01 19:46:51 -05:00
if len(curvefit.shape) != 3:
raise ValueError('Multipole curvefit arrays must be 3D')
if curvefit.shape[2] not in (2, 3): # sig_s, sig_a (maybe sig_f)
2016-12-01 19:46:51 -05:00
raise ValueError('The third dimension of multipole curvefit'
2017-05-19 16:12:24 -04:00
' arrays must have a length of 2 or 3')
2018-08-18 21:25:06 -04:00
if not np.issubdtype(curvefit.dtype, np.floating):
2016-12-01 19:46:51 -05:00
raise TypeError('Multipole curvefit arrays must be float dtype')
self._curvefit = curvefit
2016-12-01 17:18:28 -05:00
@classmethod
def from_hdf5(cls, group_or_filename):
2016-12-05 14:25:41 -05:00
"""Construct a WindowedMultipole object from an HDF5 group or file.
Parameters
----------
group_or_filename : h5py.Group or str
HDF5 group containing multipole data. If given as a string, it is
assumed to be the filename for the HDF5 file, and the first group is
used to read from.
Returns
-------
openmc.data.WindowedMultipole
Resonant cross sections represented in the windowed multipole
format.
"""
2018-03-13 21:21:12 -04:00
2016-12-01 17:18:28 -05:00
if isinstance(group_or_filename, h5py.Group):
group = group_or_filename
need_to_close = False
2016-12-01 17:18:28 -05:00
else:
2018-11-02 07:34:34 -05:00
h5file = h5py.File(str(group_or_filename), 'r')
need_to_close = True
2018-10-19 16:48:12 -04:00
# Make sure version matches
if 'version' in h5file.attrs:
major, minor = h5file.attrs['version']
if major != WMP_VERSION_MAJOR:
raise DataError(
2018-10-19 16:48:12 -04:00
'WMP data format uses version {}. {} whereas your '
'installation of the OpenMC Python API expects version '
'{}.x.'.format(major, minor, WMP_VERSION_MAJOR))
else:
raise DataError(
2018-10-19 16:48:12 -04:00
'WMP data does not indicate a version. Your installation of '
'the OpenMC Python API expects version {}.x data.'
.format(WMP_VERSION_MAJOR))
group = list(h5file.values())[0]
name = group.name[1:]
out = cls(name)
2016-12-01 19:46:51 -05:00
# Read scalars.
2016-12-01 19:46:51 -05:00
out.spacing = group['spacing'][()]
out.sqrtAWR = group['sqrtAWR'][()]
out.E_min = group['E_min'][()]
out.E_max = group['E_max'][()]
2016-12-01 17:18:28 -05:00
2016-12-01 19:46:51 -05:00
# Read arrays.
err = "WMP '{}' array shape is not consistent with the '{}' array shape"
2016-12-01 19:46:51 -05:00
out.data = group['data'][()]
out.windows = group['windows'][()]
2016-12-01 19:46:51 -05:00
2021-04-26 09:22:45 -05:00
out.broaden_poly = group['broaden_poly'][...].astype(bool)
if out.broaden_poly.shape[0] != out.windows.shape[0]:
raise ValueError(err.format('broaden_poly', 'windows'))
2016-12-01 19:46:51 -05:00
out.curvefit = group['curvefit'][()]
if out.curvefit.shape[0] != out.windows.shape[0]:
raise ValueError(err.format('curvefit', 'windows'))
# _broaden_wmp_polynomials assumes the curve fit has at least 3 terms.
if out.fit_order < 2:
raise ValueError("Windowed multipole is only supported for "
"curvefits with 3 or more terms.")
2016-12-01 17:18:28 -05:00
# If HDF5 file was opened here, make sure it gets closed
if need_to_close:
h5file.close()
2016-12-01 17:18:28 -05:00
return out
@classmethod
2020-10-13 10:28:54 +08:00
def from_endf(cls, endf_file, log=False, vf_options=None, wmp_options=None):
2019-01-02 09:28:41 -05:00
"""Generate windowed multipole neutron data from an ENDF evaluation.
.. versionadded:: 0.12.1
Parameters
----------
endf_file : str
Path to ENDF evaluation
2020-10-13 10:28:54 +08:00
log : bool or int, optional
Whether to print running logs (use int for verbosity control)
vf_options : dict, optional
Dictionary of keyword arguments, e.g. {'njoy_error': 0.001},
passed to :func:`openmc.data.multipole.vectfit_nuclide`
2020-10-13 10:28:54 +08:00
wmp_options : dict, optional
Dictionary of keyword arguments, e.g. {'search': True, 'rtol': 0.01},
2019-07-27 18:25:46 +00:00
passed to :func:`openmc.data.WindowedMultipole.from_multipole`
Returns
-------
openmc.data.WindowedMultipole
Resonant cross sections represented in the windowed multipole
format.
"""
2020-10-09 22:33:38 +08:00
if vf_options is None:
vf_options = {}
if wmp_options is None:
wmp_options = {}
2020-10-13 10:28:54 +08:00
if log:
vf_options.update(log=log)
wmp_options.update(log=log)
2019-01-02 09:28:41 -05:00
# generate multipole data from EDNF
mp_data = vectfit_nuclide(endf_file, **vf_options)
2019-01-02 09:28:41 -05:00
# windowing
2019-07-26 19:54:18 +00:00
return cls.from_multipole(mp_data, **wmp_options)
2019-01-02 09:28:41 -05:00
@classmethod
def from_multipole(
cls,
mp_data,
search=None,
log=False,
search_n_win=20,
search_cf_orders=None,
**kwargs,
):
2019-01-02 09:28:41 -05:00
"""Generate windowed multipole neutron data from multipole data.
2019-01-02 09:28:41 -05:00
Parameters
----------
mp_data : dictionary or str
2020-10-09 22:33:38 +08:00
Dictionary or Path to the multipole data stored in a pickle file
2019-07-27 03:22:45 +00:00
search : bool, optional
Whether to search for optimal window size and curvefit order.
Defaults to True if no windowing parameters are specified.
2019-07-27 18:25:46 +00:00
log : bool or int, optional
2020-10-09 22:33:38 +08:00
Whether to print running logs (use int for verbosity control)
search_n_win : int, optional
Number of window sizes to consider in the search grid when
``search`` is True.
search_cf_orders : iterable of int, optional
Curve-fit orders to consider in the search grid when ``search`` is
True. Defaults to integers from 10 down to 2.
2019-01-02 09:28:41 -05:00
**kwargs
Keyword arguments passed to :func:`openmc.data.multipole._windowing`.
2019-01-02 09:28:41 -05:00
Returns
-------
openmc.data.WindowedMultipole
Resonant cross sections represented in the windowed multipole
format.
2019-01-02 09:28:41 -05:00
"""
2019-01-02 09:28:41 -05:00
if isinstance(mp_data, str):
# load multipole data from file
with open(mp_data, 'rb') as f:
mp_data = pickle.load(f)
if search is None:
if 'n_cf' in kwargs and ('n_win' in kwargs or 'spacing' in kwargs):
search = False
else:
search = True
2019-07-27 03:22:45 +00:00
# windowing with specific options
if not search:
# set default value for curvefit order if not specified
if 'n_cf' not in kwargs:
kwargs.update(n_cf=5)
2019-07-27 03:22:45 +00:00
return _windowing(mp_data, log=log, **kwargs)
2020-02-04 16:32:12 +08:00
# search optimal WMP from a range of window sizes and CF orders
2019-07-27 03:22:45 +00:00
if log:
print("Start searching ...")
if search_cf_orders is None:
search_cf_orders = range(10, 1, -1)
2019-07-27 03:22:45 +00:00
n_poles = sum([p.size for p in mp_data["poles"]])
2020-02-04 16:32:12 +08:00
n_win_min = max(5, n_poles // 20)
2019-07-27 03:22:45 +00:00
n_win_max = 2000 if n_poles < 2000 else 8000
2020-10-09 22:33:38 +08:00
best_wmp = best_metric = None
for n_w in np.unique(
np.linspace(n_win_min, n_win_max, search_n_win, dtype=int)
):
for n_cf in search_cf_orders:
2019-07-27 03:22:45 +00:00
if log:
print(f"Testing N_win={n_w} N_cf={n_cf}")
2019-07-27 03:22:45 +00:00
# update arguments dictionary
kwargs.update(n_win=n_w, n_cf=n_cf)
2019-07-27 03:22:45 +00:00
# windowing
try:
wmp = _windowing(mp_data, log=log, **kwargs)
except Exception as e:
if log:
2020-02-06 22:11:08 +08:00
print('Failed: ' + str(e))
break
2020-02-04 16:32:12 +08:00
# select wmp library with metric:
# - performance: average # used poles per window and CF order
# - memory: # windows
metric = -(wmp.poles_per_window * 10. + wmp.fit_order * 1. +
wmp.n_windows * 0.01)
2019-07-27 03:22:45 +00:00
if best_wmp is None or metric > best_metric:
if log:
2020-10-13 10:28:54 +08:00
print("Best library so far.")
2019-07-27 03:22:45 +00:00
best_wmp = deepcopy(wmp)
best_metric = metric
2019-07-27 18:25:46 +00:00
# return the best wmp library
if log:
print("Final library: {} poles, {} windows, {:.2g} poles per window, "
2020-02-04 16:32:12 +08:00
"{} CF order".format(best_wmp.n_poles, best_wmp.n_windows,
best_wmp.poles_per_window, best_wmp.fit_order))
2019-07-27 18:25:46 +00:00
2019-07-27 03:22:45 +00:00
return best_wmp
def _evaluate(self, E, T):
"""Compute scattering, absorption, and fission cross sections.
Parameters
----------
E : Real
Energy of the incident neutron in eV.
T : Real
Temperature of the target in K.
Returns
-------
3-tuple of Real
Scattering, absorption, and fission microscopic cross sections
2023-01-22 10:07:18 -05:00
at the given energy and temperature.
"""
if E < self.E_min: return (0, 0, 0)
if E > self.E_max: return (0, 0, 0)
2016-12-01 17:18:28 -05:00
# ======================================================================
# Bookkeeping
# Define some frequently used variables.
2017-05-22 17:14:25 -04:00
sqrtkT = sqrt(K_BOLTZMANN * T)
sqrtE = sqrt(E)
2016-12-01 17:18:28 -05:00
invE = 1.0 / E
# Locate us. The i_window calc omits a + 1 present from the legacy
# Fortran version of OpenMC because of the 1-based vs. 0-based
# indexing. Similarly startw needs to be decreased by 1. endw does
# not need to be decreased because range(startw, endw) does not include
# endw.
i_window = min(self.n_windows - 1,
int(np.floor((sqrtE - sqrt(self.E_min)) / self.spacing)))
startw = self.windows[i_window, 0] - 1
endw = self.windows[i_window, 1]
2016-12-01 17:18:28 -05:00
# Initialize the ouptut cross sections.
sig_s = 0.0
sig_a = 0.0
sig_f = 0.0
2016-12-01 17:18:28 -05:00
# ======================================================================
# Add the contribution from the curvefit polynomial.
if sqrtkT != 0 and self.broaden_poly[i_window]:
# Broaden the curvefit.
dopp = self.sqrtAWR / sqrtkT
broadened_polynomials = _broaden_wmp_polynomials(E, dopp,
self.fit_order + 1)
2020-10-09 22:33:38 +08:00
for i_poly in range(self.fit_order + 1):
sig_s += (self.curvefit[i_window, i_poly, _FIT_S]
* broadened_polynomials[i_poly])
sig_a += (self.curvefit[i_window, i_poly, _FIT_A]
* broadened_polynomials[i_poly])
if self.fissionable:
sig_f += (self.curvefit[i_window, i_poly, _FIT_F]
* broadened_polynomials[i_poly])
2016-12-01 17:18:28 -05:00
else:
temp = invE
2020-10-09 22:33:38 +08:00
for i_poly in range(self.fit_order + 1):
sig_s += self.curvefit[i_window, i_poly, _FIT_S] * temp
sig_a += self.curvefit[i_window, i_poly, _FIT_A] * temp
if self.fissionable:
sig_f += self.curvefit[i_window, i_poly, _FIT_F] * temp
2016-12-01 17:18:28 -05:00
temp *= sqrtE
# ======================================================================
# Add the contribution from the poles in this window.
if sqrtkT == 0.0:
# If at 0K, use asymptotic form.
for i_pole in range(startw, endw):
2016-12-05 14:25:41 -05:00
psi_chi = -1j / (self.data[i_pole, _MP_EA] - sqrtE)
c_temp = psi_chi / E
sig_s += (self.data[i_pole, _MP_RS] * c_temp).real
sig_a += (self.data[i_pole, _MP_RA] * c_temp).real
if self.fissionable:
sig_f += (self.data[i_pole, _MP_RF] * c_temp).real
2016-12-01 19:46:51 -05:00
2016-12-01 17:18:28 -05:00
else:
# At temperature, use Faddeeva function-based form.
dopp = self.sqrtAWR / sqrtkT
for i_pole in range(startw, endw):
2016-12-05 14:25:41 -05:00
Z = (sqrtE - self.data[i_pole, _MP_EA]) * dopp
2017-05-22 17:14:25 -04:00
w_val = _faddeeva(Z) * dopp * invE * sqrt(pi)
sig_s += (self.data[i_pole, _MP_RS] * w_val).real
sig_a += (self.data[i_pole, _MP_RA] * w_val).real
if self.fissionable:
sig_f += (self.data[i_pole, _MP_RF] * w_val).real
return sig_s, sig_a, sig_f
def __call__(self, E, T):
"""Compute scattering, absorption, and fission cross sections.
Parameters
----------
E : Real or Iterable of Real
Energy of the incident neutron in eV.
T : Real
Temperature of the target in K.
Returns
-------
3-tuple of Real or 3-tuple of numpy.ndarray
2023-01-22 10:07:18 -05:00
Scattering, absorption, and fission microscopic cross sections
at the given energy and temperature.
"""
fun = np.vectorize(lambda x: self._evaluate(x, T))
return fun(E)
2018-03-13 21:21:12 -04:00
2018-10-19 16:48:12 -04:00
def export_to_hdf5(self, path, mode='a', libver='earliest'):
2018-03-13 21:21:12 -04:00
"""Export windowed multipole data to an HDF5 file.
Parameters
----------
path : str
Path to write HDF5 file to
mode : {'r+', 'w', 'x', 'a'}
2018-10-19 16:48:12 -04:00
Mode that is used to open the HDF5 file. This is the second argument
to the :class:`h5py.File` constructor.
2018-03-13 21:21:12 -04:00
libver : {'earliest', 'latest'}
Compatibility mode for the HDF5 file. 'latest' will produce files
that are less backwards compatible but have performance benefits.
"""
# Open file and write version.
2018-11-02 07:34:34 -05:00
with h5py.File(str(path), mode, libver=libver) as f:
2024-03-26 11:00:06 -05:00
f.attrs['filetype'] = np.bytes_('data_wmp')
2018-10-19 16:48:12 -04:00
f.attrs['version'] = np.array(WMP_VERSION)
2018-03-13 21:21:12 -04:00
2018-10-19 16:48:12 -04:00
g = f.create_group(self.name)
2018-03-13 21:21:12 -04:00
2018-03-19 12:55:46 -04:00
# Write scalars.
g.create_dataset('spacing', data=np.array(self.spacing))
g.create_dataset('sqrtAWR', data=np.array(self.sqrtAWR))
g.create_dataset('E_min', data=np.array(self.E_min))
g.create_dataset('E_max', data=np.array(self.E_max))
2018-03-19 12:55:46 -04:00
# Write arrays.
g.create_dataset('data', data=self.data)
g.create_dataset('windows', data=self.windows)
2018-03-19 12:55:46 -04:00
g.create_dataset('broaden_poly',
data=self.broaden_poly.astype(np.int8))
g.create_dataset('curvefit', data=self.curvefit)