Source code for scarabee.reseau.reflector

from .._scarabee import (
    NDLibrary,
    Material,
    CrossSection,
    DiffusionCrossSection,
    DiffusionData,
    ReflectorSN,
    DiffusionGeometry,
    NEM4DiffusionDriver,
    FormFactors,
    set_logging_level,
    scarabee_log,
    LogLevel,
)
from .nodal_flux import NodalFlux1D
import numpy as np

# import matplotlib.pyplot as plt


[docs]class Reflector: """ A Reflector instance is responsible for performing transport calculations necessary to produce few-group cross sections for the reflector of an LWR. The core baffle cross sections are self-shielded as an infinite slab, using the Roman two-term approximation. The calculation is performed using a 1D Sn simulation, in the group structure of the nuclear data library. This removes the need to obtain a fine-group spectrum that would be used to condense to an intermediate group structure. Parameters ---------- fuel : CrossSection Homogenized cross section which is representative of a pin cell. This is typically obtained from a previous lattice calcualtion. moderator : CrossSection Material cross sections for the moderator at desired temperature and density. assembly_width : float Width of a single fuel assembly (and the reflector to be modeled). gap_width : float Width of the moderator gap between the assembly and the core baffle. baffle_width : float Width of the core baffle. baffle : Material Material for the core baffle at desired temperature and density. ndl : NDLibrary Nuclear data library for constructing the baffle cross sections. Attributes ---------- condensation_scheme : list of pairs of ints Defines how the energy groups will be condensed to the few-group structure used in nodal calculations. fuel : CrossSection Cross sections for a homogenized fuel assembly. moderator : CrossSection Cross sections for the moderator. assembly_width : float Width of fuel assembly and reflector. gap_width : float Width of the moderator gap between a fuel assembly and the core baffle. baffle_width : float Width of the core baffle. baffle : CrossSection Self-shielded cross sections for the core baffle. nangles : int Number of angles used in the Sn solver. Default is 16. Must be one of: 2, 4, 6, 8, 10, 12, 14, 16, 32, 64, 128. anisotropic : bool If True, the reflector calculation is performed with explicit anisotropic scattering. Otherwise, the transport correction with isotropic scattering is used. Default value is False. keff_tolerance : float Convergence criteria for keff. Default is 1.E-5. flux_tolerance : float Convergence criteria for the flux. Default is 1.E-5. diffusion_xs : DiffusionCrossSection The few-group diffusion group constants for the reflector. adf : ndarray The assembly discontinuity factors. diffusion_data : DiffusionData The few-group diffusion cross sections and ADFs for the reflector. """
[docs] def __init__( self, fuel: CrossSection, moderator: CrossSection, assembly_width: float, gap_width: float, baffle_width: float, baffle: Material, ndl: NDLibrary, ): self.fuel = fuel self.fuel.name = "Fuel" self.moderator = moderator self.moderator.name = "Moderator" self.assembly_width = assembly_width self.gap_width = gap_width self.baffle_width = baffle_width self.condensation_scheme = ndl.condensation_scheme self.nangles = 16 self.anisotropic = False self.adf = None self.diffusion_xs = None self.diffusion_data = None if self.baffle_width > 0.0: # No Dancoff correction, as looking at 1D isolated slab for baffle Ee = 1.0 / (2.0 * self.baffle_width) self.baffle = baffle.roman_xs(0.0, Ee, ndl) self.baffle.name = "Baffle" else: self.baffle = None self.keff_tolerance = 1.0e-6 self.flux_tolerance = 1.0e-6 if self.gap_width + self.baffle_width > self.assembly_width: raise RuntimeError( "The assembly width is smaller than the sum of the gap and baffle widths." )
def _doctor_xs(self, xs: DiffusionCrossSection) -> DiffusionCrossSection: # First, get raw arrays from the cross section NG = xs.ngroups D = np.zeros(NG) Ea = np.zeros(NG) Ef = np.zeros(NG) vEf = np.zeros(NG) chi = np.zeros(NG) Es = np.zeros((NG, NG)) for g in range(NG): D[g] = xs.D(g) Ea[g] = xs.Ea(g) Ef[g] = xs.Ef(g) vEf[g] = xs.vEf(g) chi[g] = xs.chi(g) for gg in range(NG): Es[g, gg] = xs.Es(g, gg) D *= 1.01 return DiffusionCrossSection(D, Ea, Es, Ef, vEf, chi)
[docs] def solve(self) -> None: """ Runs a 1D problem to generate few group cross sections for the reflector, with the core baffle. """ scarabee_log(LogLevel.Info, "Starting reflector calculation.") if self.condensation_scheme is None: raise RuntimeError( "Cannot perform reflector calculation without condensation scheme." ) # We start by making a ReflectorSn to do 1D calculation dx = [] mats = [] # We add 1 fuel assembly worth of homogenized core NF = 15 * 17 dr = self.assembly_width / float(NF) dx += [dr] * NF mats += [self.fuel] * NF # We now add the gap (if present) if self.gap_width == 0.0: NG = 0 else: NG = int(self.gap_width / 0.1) + 1 if NG < 5: NG = 5 dr = self.gap_width / float(NG) dx += [dr] * NG mats += [self.moderator] * NG # Now we add regions for the baffle if self.baffle_width == 0.0: NB = 0 else: NB = int(self.baffle_width / 0.1) + 1 dr = self.baffle_width / float(NB) dx += [dr] * NB mats += [self.baffle] * NB # Now we add the outer water reflector regions ref_width = self.assembly_width - self.gap_width - self.baffle_width if ref_width <= 0.0: # Case of a heavy reflector NR = 0 else: NR = int(ref_width / 0.1) + 1 dr = ref_width / float(NR) dx += [dr] * NR mats += [self.moderator] * NR # Setup the Sn solver and run ref_sn = ReflectorSN(mats, dx, self.nangles, self.anisotropic) set_logging_level(LogLevel.Warning) ref_sn.solve() set_logging_level(LogLevel.Info) scarabee_log(LogLevel.Info, "") scarabee_log(LogLevel.Info, "SN Keff: {:.5f}".format(ref_sn.keff)) # Here we compute the homogenized and condensed cross sections ref_homog_xs = ref_sn.homogenize(list(range(NF, NF + NG + NB + NR))) ref_homog_spec = ref_sn.homogenize_flux_spectrum( list(range(NF, NF + NG + NB + NR)) ) ref_homog_diff_xs = ref_homog_xs.diffusion_xs() self.diffusion_xs = ref_homog_diff_xs.condense( self.condensation_scheme, ref_homog_spec ) fuel_homog_xs = ref_sn.homogenize(list(range(0, NF))) fuel_homog_spec = ref_sn.homogenize_flux_spectrum(list(range(0, NF))) fuel_homog_diff_xs = fuel_homog_xs.diffusion_xs() fuel_diffusion_xs = fuel_homog_diff_xs.condense( self.condensation_scheme, fuel_homog_spec ) # Condense the few-group flux along entire 1D geometry few_group_flux = np.zeros((len(self.condensation_scheme), NF + NG + NB + NR)) for i in range(NF + NG + NB + NR): for G in range(len(self.condensation_scheme)): g_min = self.condensation_scheme[G][0] g_max = self.condensation_scheme[G][1] for g in range(g_min, g_max + 1): few_group_flux[G, i] += ref_sn.flux(i, g) dx = np.array(dx) # Make an array with the mid-point x-values of each bin from Sn simulation x = np.zeros(len(dx)) for i in range(len(dx)): if i == 0: x[0] = 0.5 * dx[0] else: x[i] = x[i - 1] + 0.5 * (dx[i - 1] + dx[i]) # Obtain net currents at node boundaries and average flux avg_flx_ref = np.zeros(len(self.condensation_scheme)) avg_flx_fuel = np.zeros(len(self.condensation_scheme)) j_0 = np.zeros(len(self.condensation_scheme)) j_mid = np.zeros(len(self.condensation_scheme)) j_max = np.zeros(len(self.condensation_scheme)) s_mid = NF s_max = ref_sn.nsurfaces - 1 len_fuel = np.sum(dx[:NF]) len_ref = np.sum(dx[NF:]) for g in range(len(self.condensation_scheme)): avg_flx_fuel[g] = np.sum(few_group_flux[g, :NF] * dx[:NF]) / len_fuel avg_flx_ref[g] = np.sum(few_group_flux[g, NF:] * dx[NF:]) / len_ref g_min = self.condensation_scheme[g][0] g_max = self.condensation_scheme[g][1] for gg in range(g_min, g_max + 1): j_0[g] += ref_sn.current(0, gg) j_mid[g] += ref_sn.current(s_mid, gg) j_max[g] += ref_sn.current(s_max, gg) # Do nodal calculation to obtain homogeneous flux x_fuel = np.sum(dx[:NF]) x_ref_end = np.sum(dx) fuel_node = NodalFlux1D( 0.0, x_fuel, ref_sn.keff, fuel_diffusion_xs, avg_flx_fuel, j_0, j_mid ) ref_node = NodalFlux1D( x_fuel, x_ref_end, ref_sn.keff, self.diffusion_xs, avg_flx_ref, j_mid, j_max ) # I noticed that the initial fixed-source claculation from above can result in # a negative flux next to the vacuum BC, which is of course non-physical. Here, # we iteratively bump up the diffusion coefficients until the fluxes are # positive at the far right boundary in all groups. xmax_homog_fluxes = ref_node.pos_surf_flux() while np.min(xmax_homog_fluxes) < 0.0: self.diffusion_xs = self._doctor_xs(self.diffusion_xs) ref_node = NodalFlux1D( x_fuel, x_ref_end, ref_sn.keff, self.diffusion_xs, avg_flx_ref, j_mid, j_max, ) xmax_homog_fluxes = ref_node.pos_surf_flux() # Compute DFs heter_flx = 0.5 * (few_group_flux[:, NF - 1] + few_group_flux[:, NF]) homog_flx_fuel = fuel_node.pos_surf_flux() homog_flx_ref = ref_node.neg_surf_flux() # Get DF for fuel and reflector sides f_fuel = heter_flx / homog_flx_fuel f_ref = heter_flx / homog_flx_ref # As outlined by Smith in [1], we should normalize the reflector DFs by # The fuel DFs from the reflector calculation. Then, in the nodal # calculation, we can multiply by the DF of the actual fuel next to the # node to get better results. f_ref /= f_fuel f_fuel /= f_fuel nodal_flux = np.zeros((len(self.condensation_scheme), len(x))) self.adf = np.ones((len(self.condensation_scheme), 6)) fuel_adf = np.ones((len(self.condensation_scheme), 6)) for g in range(len(self.condensation_scheme)): nodal_flux[g, :NF] = fuel_node(x[:NF], g) nodal_flux[g, NF:] = ref_node(x[NF:], g) self.adf[g, :] = f_ref[g] fuel_adf[g, :] = f_fuel[g] # Create the diffusion data self.diffusion_data = DiffusionData(self.diffusion_xs) self.diffusion_data.adf = self.adf self.diffusion_data.reflector = True self.form_factors = FormFactors( np.array([[0.0]]), np.array([self.assembly_width]), np.array([self.assembly_width]), ) # Do a nodal k-eff calulation fuel_diffision_data = DiffusionData(fuel_diffusion_xs) fuel_diffision_data.adf = fuel_adf dx = np.array([1 * self.assembly_width, self.assembly_width]) nx = np.array([1, 1]) dy = np.array([10.0]) ny = np.array([1]) nem_geom = DiffusionGeometry( [fuel_diffision_data, self.diffusion_data], dx, nx, dy, ny, dy, ny, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, ) nem_solver = NEM4DiffusionDriver(nem_geom) nem_solver.nonlinear_update_frequency = 1 nem_solver.source_extrapolation_frequency = 100 nem_solver.flux_tolerance = 1.0e-7 set_logging_level(LogLevel.Warning) nem_solver.solve() set_logging_level(LogLevel.Info) scarabee_log(LogLevel.Info, "NEM Keff: {:.5f}".format(nem_solver.keff)) nem_keff_flux = nem_solver.flux(x, [10.0], [10.0]) flux_norm = np.sum(few_group_flux[:, :NF]) nem_keff_flux *= flux_norm / np.sum(nem_keff_flux[:, :NF])
# Plot flux for each group # for g in range(len(self.condensation_scheme)): # plt.plot(x, few_group_flux[g, :], label="Sn") # plt.plot(x, nodal_flux[g, :], label="NEM Fixed-Source") # plt.plot(x, nem_keff_flux[g, :, 0, 0], label="NEM keff") # plt.xlabel("x [cm]") # plt.ylabel("Flux [Arb. Units]") # plt.title("Group {:}".format(g)) # plt.legend().set_draggable(True) # plt.tight_layout() # plt.show() # [1] K. S. Smith, “Nodal diffusion methods and lattice physics data in LWR # analyses: Understanding numerous subtle details,” Prog Nucl Energ, # vol. 101, pp. 360–369, 2017, doi: 10.1016/j.pnucene.2017.06.013.