Source code for scarabee.reseau.fuel_pin

from .._scarabee import (
    NDLibrary,
    MaterialComposition,
    Material,
    CrossSection,
    PinCellType,
    SimplePinCell,
    PinCell,
    MOCDriver,
    MixingFraction,
    DepletionChain,
    DepletionMatrix,
    mix_materials,
    build_depletion_matrix,
)
import numpy as np
from typing import Optional, List
import copy


[docs]class FuelPin: """ Represents a generic fuel pin for a PWR. Parameters ---------- fuel : Material Material which describes the fuel composition, density, and temperature. fuel_radius : float Outer radius of the fuel pellet region. gap : Material, optional Material which describes the composition, density, and temperature of the gap between the fuel pellet and the cladding, if present. gap_radius : float, optional Outer radius of the gap material, if present. clad : Material Material which describes the cladding composition, density, and temperature. clad_radius : float Outer radius of the cladding. num_fuel_rings : int, default 1 Number of rings which should be used to discretize the fuel material. Each ring will be self-shielded and depleted separately. Attributes ---------- fuel_radius : float Outer radius of the fuel pellet region. fuel_ring_materials : list of list of Material Contains the Material in each fuel ring for each depletion time step. fuel_ring_flux_spectra : list of list of ndarray Contains the average flux spectrum in each fuel ring for each depletion time step. fuel_dancoff_corrections : list of float Dancoff corrections to be used when self-shielding the fuel at each depletion time step. gap : Material, optional Material which describes the composition, density, and temperature of the gap between the fuel pellet and the cladding, if present. gap_radius : float, optional Outer radius of the gap material, if present. clad : Material Material which describes the cladding composition, density, and temperature. clad_radius : float Outer radius of the cladding. clad_dancoff_corrections : list of float Dancoff corrections to be used when self-shielding the cladding at each depletion time step. num_fuel_rings : int, default 1 Number of rings which should be used to discretize the fuel material. Each ring will be self-shielded and depleted separately. """
[docs] def __init__( self, fuel: Material, fuel_radius: float, clad: Material, clad_radius, gap: Optional[Material], gap_radius: Optional[float], num_fuel_rings: int = 1, ): if fuel_radius <= 0.0: raise ValueError("Fuel radius must be > 0.") self._fuel_radius = fuel_radius if num_fuel_rings <= 0: raise ValueError("Number of fuel rings must be >= 1.") self._num_fuel_rings = num_fuel_rings # Mass of fissionable matter in g / cm (i.e. per unit length) self._initial_fissionable_linear_mass = ( fuel.fissionable_grams_per_cm3 * np.pi * self._fuel_radius**2.0 ) # Get gap related parameters if gap is None and gap_radius is not None: raise ValueError("Gap material is None but gap radius is defined.") elif gap is not None and gap_radius is None: raise ValueError("Gap material is defined but gap radius is None.") if gap_radius is not None and gap_radius <= fuel_radius: raise ValueError("Gap radius must be > fuel radius.") self._gap = copy.deepcopy(gap) self._gap_radius = gap_radius # Get cladding related parameters self._clad = copy.deepcopy(clad) if clad_radius <= 0.0 or clad_radius <= fuel_radius: raise ValueError("Clad radius must be > fuel radius.") elif gap_radius is not None and clad_radius <= gap_radius: raise ValueError("Clad radius must be > gap radius.") self._clad_radius = clad_radius # ====================================================================== # DANCOFF CORRECTION CALCULATION DATA # ---------------------------------------------------------------------- # Initialize empty list of Dancoff corrections for the fuel self._fuel_dancoff_corrections: List[float] = [] # Initialize empty list of Dancoff corrections for the cladding self._clad_dancoff_corrections: List[float] = [] # Initialize empty variables for Dancoff correction calculations. # These are all kept private. self._fuel_dancoff_xs: CrossSection = CrossSection( np.array([1.0e5]), np.array([1.0e5]), np.array([[0.0]]), "Fuel" ) self._gap_dancoff_xs: Optional[CrossSection] = None if self.gap is not None: self._gap_dancoff_xs = CrossSection( np.array([self.gap.potential_xs]), np.array([self.gap.potential_xs]), np.array([[0.0]]), "Gap", ) self._clad_dancoff_xs: CrossSection = CrossSection( np.array([self.clad.potential_xs]), np.array([self.clad.potential_xs]), np.array([[0.0]]), "Clad", ) self._fuel_isolated_dancoff_fsr_ids = [] self._gap_isolated_dancoff_fsr_ids = [] self._clad_isolated_dancoff_fsr_ids = [] self._mod_isolated_dancoff_fsr_ids = [] self._fuel_full_dancoff_fsr_ids = [] self._gap_full_dancoff_fsr_ids = [] self._clad_full_dancoff_fsr_ids = [] self._mod_full_dancoff_fsr_ids = [] self._fuel_isolated_dancoff_fsr_inds = [] self._gap_isolated_dancoff_fsr_inds = [] self._clad_isolated_dancoff_fsr_inds = [] self._mod_isolated_dancoff_fsr_inds = [] self._fuel_full_dancoff_fsr_inds = [] self._gap_full_dancoff_fsr_inds = [] self._clad_full_dancoff_fsr_inds = [] self._mod_full_dancoff_fsr_inds = [] # ====================================================================== # TRANSPORT CALCULATION DATA # ---------------------------------------------------------------------- # Lists of the FSR IDs for each fuel ring, used to homogenize flux # spectra for depletion. These will be filled by make_moc_cell. self._fuel_ring_fsr_ids: List[List[int]] = [] for r in range(self.num_fuel_rings): self._fuel_ring_fsr_ids.append([]) self._gap_fsr_ids: List[int] = [] self._clad_fsr_ids: List[int] = [] self._mod_fsr_ids: List[int] = [] self._fuel_ring_fsr_inds: List[List[int]] = [] for r in range(self.num_fuel_rings): self._fuel_ring_fsr_inds.append([]) self._gap_fsr_inds: List[int] = [] self._clad_fsr_inds: List[int] = [] self._mod_fsr_inds: List[int] = [] # Create list of the different radii for fuel pellet self._fuel_radii = [] if self.num_fuel_rings == 1: self._fuel_radii.append(self.fuel_radius) else: V = np.pi * self.fuel_radius * self.fuel_radius Vr = V / self.num_fuel_rings for ri in range(self.num_fuel_rings): Rin = 0.0 if ri > 0: Rin = self._fuel_radii[-1] Rout = np.sqrt((Vr + np.pi * Rin * Rin) / np.pi) if Rout > self.fuel_radius: Rout = self.fuel_radius self._fuel_radii.append(Rout) # Initialize array of compositions for the fuel. This holds the # composition for each fuel ring and for each depletion step. self._fuel_ring_materials: List[List[Material]] = [] for r in range(self.num_fuel_rings): # All rings initially start with the same composition self._fuel_ring_materials.append([copy.deepcopy(fuel)]) # Initialize an array to hold the flux spectrum for each fuel ring. self._fuel_ring_flux_spectra: List[np.ndarray] = [] for r in range(self.num_fuel_rings): # All rings initially start with empty flux spectrum list self._fuel_ring_flux_spectra.append(np.array([])) # Initialize an array to hold the depletion matrices for the previous and current steps. self._fuel_ring_prev_dep_mats: List[Optional[DepletionMatrix]] = [] self._fuel_ring_current_dep_mats: List[Optional[DepletionMatrix]] = [] for r in range(self.num_fuel_rings): # All rings initially start with empty matrix self._fuel_ring_prev_dep_mats.append(None) self._fuel_ring_current_dep_mats.append(None) # Holds all the CrossSection objects used for the real transport # calculation. These are NOT stored for each depletion step like with # the materials. self._fuel_ring_xs: List[CrossSection] = [] self._gap_xs: Optional[CrossSection] = None self._clad_xs: Optional[CrossSection] = None
@property def fuel_radius(self) -> float: return self._fuel_radius @property def num_fuel_rings(self) -> int: return self._num_fuel_rings @property def initial_fissionable_linear_mass(self) -> float: return self._initial_fissionable_linear_mass @property def fuel_ring_materials(self) -> List[List[Material]]: return self._fuel_ring_materials @property def fuel_ring_flux_spectra(self) -> List[List[Material]]: return self._fuel_ring_materials @property def fuel_dancoff_corrections(self) -> List[float]: return self._fuel_dancoff_corrections @property def gap(self) -> Optional[Material]: return self._gap @property def gap_radius(self) -> Optional[float]: return self._gap_radius @property def clad(self) -> Material: return self._clad @property def clad_radius(self) -> float: return self._clad_radius @property def clad_dancoff_corrections(self) -> List[float]: return self._clad_dancoff_corrections def _check_dx_dy(self, dx, dy, pintype): if pintype == PinCellType.Full: if dx < 2.0 * self.clad_radius: raise ValueError( "The fuel pin cell x width must be > the diameter of the cladding." ) if dy < 2.0 * self.clad_radius: raise ValueError( "The fuel pin cell y width must be > the diameter of the cladding." ) elif pintype in [PinCellType.XN, PinCellType.XP]: if dx < self.clad_radius: raise ValueError( "The fuel pin cell x width must be > the radius of the cladding." ) if dy < 2.0 * self.clad_radius: raise ValueError( "The fuel pin cell y width must be > the diameter of the cladding." ) elif pintype in [PinCellType.YN, PinCellType.YP]: if dy < self.clad_radius: raise ValueError( "The fuel pin cell y width must be > the radius of the cladding." ) if dx < 2.0 * self.clad_radius: raise ValueError( "The fuel pin cell x width must be > the diameter of the cladding." ) else: if dx < self.clad_radius: raise ValueError( "The fuel pin cell x width must be > the radius of the cladding." ) if dy < self.clad_radius: raise ValueError( "The fuel pin cell y width must be > the radius of the cladding." )
[docs] def load_nuclides(self, ndl: NDLibrary) -> None: """ Loads all the nuclides for all current materials into the data library. Parameters ---------- ndl : NDLibrary Nuclear data library which should load the nuclides. """ for ring_mats in self.fuel_ring_materials: ring_mats[-1].load_nuclides(ndl) if self.gap is not None: self.gap.load_nuclides(ndl) self.clad.load_nuclides(ndl)
# ========================================================================== # Interrogation Methods
[docs] def get_fuel_material(self, t: int, r: int = 0) -> Material: """ Returns the Material object for a desired fuel ring at a desired depletion time step. Ring index 0 is at the center of the pin. Parameters ---------- t : int Depletion time step index. r : int Ring index. Default is 0. Returns ------- Material Material defining the temperature, density, and composition for the desired fuel ring and depletion time step. """ if r >= self.num_fuel_rings: raise IndexError(f"Fuel ring index {r} is out of range.") if t >= len(self._fuel_ring_materials[r]): raise IndexError(f"Fuel time step index {t} is out of range.") return self._fuel_ring_materials[r][t]
[docs] def get_average_fuel_nuclide_density(self, t: int, nuclide: str) -> float: """ Computes the average density of a nuclide within the fuel pellet at a single depletion time step. Parameters ---------- t : int Depletion time step index. nuclide : str Name of the nuclide. Returns ------- float Average density of the nuclide at depletion time step t across the fuel pellet in units of atoms per barn-cm. """ sum_density = 0.0 for r in range(self.num_fuel_rings): mat = self.get_fuel_material(t, r) sum_density += mat.atom_density(nuclide) return sum_density / self.num_fuel_rings
# ========================================================================== # Dancoff Correction Related Methods
[docs] def set_xs_for_fuel_dancoff_calculation(self) -> None: """ Sets the 1-group cross sections to calculate the fuel Dancoff correction. """ self._fuel_dancoff_xs.set( CrossSection( np.array([1.0e5]), np.array([1.0e5]), np.array([[0.0]]), "Fuel" ) ) if self._gap_dancoff_xs is not None and self.gap is not None: self._gap_dancoff_xs.set( CrossSection( np.array([self.gap.potential_xs]), np.array([self.gap.potential_xs]), np.array([[0.0]]), "Gap", ) ) self._clad_dancoff_xs.set( CrossSection( np.array([self.clad.potential_xs]), np.array([self.clad.potential_xs]), np.array([[0.0]]), "Clad", ) )
[docs] def set_xs_for_clad_dancoff_calculation(self, ndl: NDLibrary) -> None: """ Sets the 1-group cross sections to calculate the clad Dancoff correction. Parameters ---------- ndl : NDLibrary Nuclear data library for obtaining potential scattering cross sections. """ # Create average fuel mixture fuel_mats = [] fuel_vols = [] for ring in self.fuel_ring_materials: fuel_mats.append(ring[-1]) fuel_vols.append(1.0 / self.num_fuel_rings) avg_fuel: Material = mix_materials( fuel_mats, fuel_vols, MixingFraction.Volume, ndl ) self._fuel_dancoff_xs.set( CrossSection( np.array([avg_fuel.potential_xs]), np.array([avg_fuel.potential_xs]), np.array([[0.0]]), "Fuel", ) ) if self._gap_dancoff_xs is not None and self.gap is not None: self._gap_dancoff_xs.set( CrossSection( np.array([self.gap.potential_xs]), np.array([self.gap.potential_xs]), np.array([[0.0]]), "Gap", ) ) self._clad_dancoff_xs.set( CrossSection( np.array([1.0e5]), np.array([1.0e5]), np.array([[0.0]]), "Clad", ) )
[docs] def make_dancoff_moc_cell( self, moderator_xs: CrossSection, dx: float, dy: float, pintype: PinCellType, isolated: bool, ) -> SimplePinCell: """ Makes a simplified cell suitable for performing Dancoff correction calculations. The flat source region IDs are stored locally in the FuelPin object. Parameters ---------- moderator_xs : CrossSection One group cross sections for the moderator. Total should equal absorption (i.e. no scattering) and should be equal to the macroscopic potential cross section. dx : float Width of the cell along x. dy : float Width of the cell along y. pintype : PinCellType How the pin cell should be split (along x, y, or only a quadrant). isolated : bool If True, the FSR IDs are stored for the isolated pin. Otherwise, they are stored for the full pin. Returns ------- SimplifiedPinCell Pin cell object for MOC Dancoff correction calculation. """ self._check_dx_dy(dx, dy, pintype) # First we create list of radii and materials radii = [] xs = [] radii.append(self.fuel_radius) xs.append(self._fuel_dancoff_xs) if self.gap is not None and self.gap_radius is not None: radii.append(self.gap_radius) xs.append(self._gap_dancoff_xs) radii.append(self.clad_radius) xs.append(self._clad_dancoff_xs) xs.append(moderator_xs) # Make the simple pin cell. cell = SimplePinCell(radii, xs, dx, dy, pintype) # Get the FSR IDs for the regions of interest cell_fsr_ids = cell.get_all_fsr_ids() if isolated: self._fuel_isolated_dancoff_fsr_ids.append(cell_fsr_ids[0]) if self.gap is None: self._clad_isolated_dancoff_fsr_ids.append(cell_fsr_ids[1]) self._mod_isolated_dancoff_fsr_ids.append(cell_fsr_ids[2]) else: self._gap_isolated_dancoff_fsr_ids.append(cell_fsr_ids[1]) self._clad_isolated_dancoff_fsr_ids.append(cell_fsr_ids[2]) self._mod_isolated_dancoff_fsr_ids.append(cell_fsr_ids[3]) else: self._fuel_full_dancoff_fsr_ids.append(cell_fsr_ids[0]) if self.gap is None: self._clad_full_dancoff_fsr_ids.append(cell_fsr_ids[1]) self._mod_full_dancoff_fsr_ids.append(cell_fsr_ids[2]) else: self._gap_full_dancoff_fsr_ids.append(cell_fsr_ids[1]) self._clad_full_dancoff_fsr_ids.append(cell_fsr_ids[2]) self._mod_full_dancoff_fsr_ids.append(cell_fsr_ids[3]) return cell
[docs] def populate_dancoff_fsr_indexes( self, isomoc: MOCDriver, fullmoc: MOCDriver ) -> None: """ Obtains the flat source region indexes for all of the flat source regions used in the Dancoff correction calculations. Parameters ---------- isomoc : MOCDriver MOC simulation for the isolated pin. fullmoc : MOCDriver MOC simulation for the full geometry. """ self._fuel_isolated_dancoff_fsr_inds = [] self._gap_isolated_dancoff_fsr_inds = [] self._clad_isolated_dancoff_fsr_inds = [] self._mod_isolated_dancoff_fsr_inds = [] self._fuel_full_dancoff_fsr_inds = [] self._gap_full_dancoff_fsr_inds = [] self._clad_full_dancoff_fsr_inds = [] self._mod_full_dancoff_fsr_inds = [] for id in self._fuel_isolated_dancoff_fsr_ids: self._fuel_isolated_dancoff_fsr_inds.append(isomoc.get_fsr_indx(id, 0)) for id in self._gap_isolated_dancoff_fsr_ids: self._gap_isolated_dancoff_fsr_inds.append(isomoc.get_fsr_indx(id, 0)) for id in self._clad_isolated_dancoff_fsr_ids: self._clad_isolated_dancoff_fsr_inds.append(isomoc.get_fsr_indx(id, 0)) for id in self._mod_isolated_dancoff_fsr_ids: self._mod_isolated_dancoff_fsr_inds.append(isomoc.get_fsr_indx(id, 0)) for id in self._fuel_full_dancoff_fsr_ids: self._fuel_full_dancoff_fsr_inds.append(fullmoc.get_fsr_indx(id, 0)) for id in self._gap_full_dancoff_fsr_ids: self._gap_full_dancoff_fsr_inds.append(fullmoc.get_fsr_indx(id, 0)) for id in self._clad_full_dancoff_fsr_ids: self._clad_full_dancoff_fsr_inds.append(fullmoc.get_fsr_indx(id, 0)) for id in self._mod_full_dancoff_fsr_ids: self._mod_full_dancoff_fsr_inds.append(fullmoc.get_fsr_indx(id, 0))
[docs] def set_isolated_dancoff_fuel_sources( self, isomoc: MOCDriver, moderator: Material ) -> None: """ Initializes the fixed sources for the isolated MOC calculation required in computing Dancoff corrections. Sources are set for a fuel Dancoff corrections calculation. Parameters ---------- isomoc : MOCDriver MOC simulation for the isolated geometry. moderator : Material Material definition for the moderator, used to obtain the potential scattering cross section. """ # Fuel sources should all be zero ! for ind in self._fuel_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, 0.0) # Gap sources should all be potential_xs if self.gap is not None: pot_xs = self.gap.potential_xs for ind in self._gap_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, pot_xs) # Clad sources should all be potential_xs pot_xs = self.clad.potential_xs for ind in self._clad_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, pot_xs) # Moderator sources should all be potential_xs pot_xs = moderator.potential_xs for ind in self._mod_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, pot_xs)
[docs] def set_isolated_dancoff_clad_sources( self, isomoc: MOCDriver, moderator: Material, ndl: NDLibrary ) -> None: """ Initializes the fixed sources for the isolated MOC calculation required in computing Dancoff corrections. Sources are set for a clad Dancoff correction calculation. Parameters ---------- isomoc : MOCDriver MOC simulation for the isolated geometry. moderator : Material Material definition for the moderator, used to obtain the potential scattering cross section. ndl : NDLibrary Nuclear data library for obtaining potential scattering cross sections. """ # Create average fuel mixture fuel_mats = [] fuel_vols = [] for ring in self.fuel_ring_materials: fuel_mats.append(ring[-1]) fuel_vols.append(1.0 / self.num_fuel_rings) avg_fuel: Material = mix_materials( fuel_mats, fuel_vols, MixingFraction.Volume, ndl ) # Fuel sources should all be potential_xs pot_xs = avg_fuel.potential_xs for ind in self._fuel_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, pot_xs) # Gap sources should all be potential_xs if self.gap is not None: pot_xs = self.gap.potential_xs for ind in self._gap_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, pot_xs) # Clad sources should all be zero ! for ind in self._clad_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, 0.0) # Moderator sources should all be potential_xs pot_xs = moderator.potential_xs for ind in self._mod_isolated_dancoff_fsr_inds: isomoc.set_extern_src(ind, 0, pot_xs)
[docs] def set_full_dancoff_fuel_sources( self, fullmoc: MOCDriver, moderator: Material ) -> None: """ Initializes the fixed sources for the full MOC calculation required in computing Dancoff corrections. Sources are set for a fuel Dancoff correction calculation. Parameters ---------- fullmoc : MOCDriver MOC simulation for the full geometry. moderator : Material Material definition for the moderator, used to obtain the potential scattering cross section. """ # Fuel sources should all be zero ! for ind in self._fuel_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, 0.0) # Gap sources should all be potential_xs if self.gap is not None: pot_xs = self.gap.potential_xs for ind in self._gap_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, pot_xs) # Clad sources should all be potential_xs pot_xs = self.clad.potential_xs for ind in self._clad_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, pot_xs) # Moderator sources should all be potential_xs pot_xs = moderator.potential_xs for ind in self._mod_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, pot_xs)
[docs] def set_full_dancoff_clad_sources( self, fullmoc: MOCDriver, moderator: Material, ndl: NDLibrary ) -> None: """ Initializes the fixed sources for the full MOC calculation required in computing Dancoff corrections. Sources are set for a clad Dancoff correction calculation. Parameters ---------- isomoc : MOCDriver MOC simulation for the isolated geometry. moderator : Material Material definition for the moderator, used to obtain the potential scattering cross section. ndl : NDLibrary Nuclear data library for obtaining potential scattering cross sections. """ # Create average fuel mixture fuel_mats = [] fuel_vols = [] for ring in self.fuel_ring_materials: fuel_mats.append(ring[-1]) fuel_vols.append(1.0 / self.num_fuel_rings) avg_fuel: Material = mix_materials( fuel_mats, fuel_vols, MixingFraction.Volume, ndl ) # Fuel sources should all be potential_xs pot_xs = avg_fuel.potential_xs for ind in self._fuel_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, pot_xs) # Gap sources should all be potential_xs if self.gap is not None: pot_xs = self.gap.potential_xs for ind in self._gap_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, pot_xs) # Clad sources should all be zero ! for ind in self._clad_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, 0.0) # Moderator sources should all be potential_xs pot_xs = moderator.potential_xs for ind in self._mod_full_dancoff_fsr_inds: fullmoc.set_extern_src(ind, 0, pot_xs)
[docs] def compute_fuel_dancoff_correction( self, isomoc: MOCDriver, fullmoc: MOCDriver ) -> float: """ Computes the Dancoff correction for the fuel region of the fuel pin. Parameters ---------- isomoc : MOCDriver MOC simulation for the isolated geometry (previously solved). fullmoc : MOCDriver MOC simulation for the full geometry (previously solved). Returns ------- float Dancoff correction for the fuel region. """ iso_flux = isomoc.homogenize_flux_spectrum( self._fuel_isolated_dancoff_fsr_inds )[0] full_flux = fullmoc.homogenize_flux_spectrum(self._fuel_full_dancoff_fsr_inds)[ 0 ] return (iso_flux - full_flux) / iso_flux
[docs] def compute_clad_dancoff_correction( self, isomoc: MOCDriver, fullmoc: MOCDriver ) -> float: """ Computes the Dancoff correction for the cladding region of the fuel pin. Parameters ---------- isomoc : MOCDriver MOC simulation for the isolated geometry (previously solved). fullmoc : MOCDriver MOC simulation for the full geometry (previously solved). Returns ------- float Dancoff correction for the cladding region. """ iso_flux = isomoc.homogenize_flux_spectrum( self._clad_isolated_dancoff_fsr_inds )[0] full_flux = fullmoc.homogenize_flux_spectrum(self._clad_full_dancoff_fsr_inds)[ 0 ] return (iso_flux - full_flux) / iso_flux
[docs] def append_fuel_dancoff_correction(self, C) -> None: """ Saves new Dancoff correction for the fuel that will be used for all subsequent cross section updates. Parameters ---------- C : float New Dancoff correction. """ if C < 0.0 or C > 1.0: raise ValueError( f"Dancoff correction must be in range [0, 1]. Was provided {C}." ) self._fuel_dancoff_corrections.append(C)
[docs] def append_clad_dancoff_correction(self, C) -> None: """ Saves new Dancoff correction for the cladding that will be used for all subsequent cross section updates. Parameters ---------- C : float New Dancoff correction. """ if C < 0.0 or C > 1.0: raise ValueError( f"Dancoff correction must be in range [0, 1]. Was provided {C}." ) self._clad_dancoff_corrections.append(C)
# ========================================================================== # Transport Calculation Related Methods
[docs] def set_fuel_xs_for_depletion_step(self, t: int, ndl: NDLibrary) -> None: """ Constructs the CrossSection object for all fuel rings of the pin at the specified depletion step. Parameters ---------- t : int Index for the depletion step. ndl : NDLibrary Nuclear data library to use for cross sections. """ # Do the fuel cross sections if len(self._fuel_ring_xs) == 0: # Create initial CrossSection objects if self.num_fuel_rings == 1: # Compute escape xs Ee = 1.0 / (2.0 * self.fuel_radius) self._fuel_ring_xs.append( self._fuel_ring_materials[0][t].carlvik_xs( self._fuel_dancoff_corrections[t], Ee, ndl ) ) if self._fuel_ring_xs[-1].name == "": self._fuel_ring_xs[-1].name = "Fuel" else: # Do each ring for ri in range(self.num_fuel_rings): Rin = 0.0 if ri > 0: Rin = self._fuel_radii[ri - 1] Rout = self._fuel_radii[ri] self._fuel_ring_xs.append( self._fuel_ring_materials[ri][t].ring_carlvik_xs( self._fuel_dancoff_corrections[t], self.fuel_radius, Rin, Rout, ndl, ) ) if self._fuel_ring_xs[-1].name == "": self._fuel_ring_xs[-1].name = "Fuel" elif len(self._fuel_ring_xs) == self.num_fuel_rings: # Reset XS values. Cannot reassign or pointers will be broken ! if self.num_fuel_rings == 1: # Compute escape xs Ee = 1.0 / (2.0 * self.fuel_radius) self._fuel_ring_xs[0].set( self._fuel_ring_materials[0][t].carlvik_xs( self._fuel_dancoff_corrections[t], Ee, ndl ) ) if self._fuel_ring_xs[0].name == "": self._fuel_ring_xs[0].name = "Fuel" else: # Do each ring for ri in range(self.num_fuel_rings): Rin = 0.0 if ri > 0: Rin = self._fuel_radii[ri - 1] Rout = self._fuel_radii[ri] self._fuel_ring_xs[ri].set( self._fuel_ring_materials[ri][t].ring_carlvik_xs( self._fuel_dancoff_corrections[t], self.fuel_radius, Rin, Rout, ndl, ) ) if self._fuel_ring_xs[ri].name == "": self._fuel_ring_xs[ri].name = "Fuel" else: raise RuntimeError( "Number of fuel cross sections does not agree with the number of fuel rings." )
[docs] def set_gap_xs(self, ndl: NDLibrary) -> None: """ Constructs the CrossSection object for the gap between the fuel pellet and the cladding of the pin. Parameters ---------- ndl : NDLibrary Nuclear data library to use for cross sections. """ if self.gap is not None: if self._gap_xs is None: self._gap_xs = self.gap.dilution_xs([1.0e10] * self.gap.size, ndl) else: self._gap_xs.set(self.gap.dilution_xs([1.0e10] * self.gap.size, ndl)) if self._gap_xs.name == "": self._gap_xs.name = "Gap"
[docs] def set_clad_xs_for_depletion_step(self, t: int, ndl: NDLibrary) -> None: """ Constructs the CrossSection object for the cladding of the pin at the specified depletion step. The depletion step only changes the Dancoff correction, not the cladding composition. Parameters ---------- t : int Index for the depletion step. ndl : NDLibrary Nuclear data library to use for cross sections. """ # Compute escape xs Ee = 0.0 if self.gap_radius is not None: Ee = 1.0 / (2.0 * (self.clad_radius - self.gap_radius)) else: Ee = 1.0 / (2.0 * (self.clad_radius - self.fuel_radius)) # Get / set the xs if self._clad_xs is None: self._clad_xs = self.clad.roman_xs( self._clad_dancoff_corrections[t], Ee, ndl ) else: self._clad_xs.set( self.clad.roman_xs(self._clad_dancoff_corrections[t], Ee, ndl) ) if self._clad_xs.name == "": self._clad_xs.name = "Clad"
[docs] def make_moc_cell( self, moderator_xs: CrossSection, dx: float, dy: float, pintype: PinCellType, ) -> PinCell: """ Constructs the pin cell object used in for the global MOC simulation. Parameters ---------- moderator_xs : CrossSection Cross sections to use for the moderator surrounding the fuel pin. dx : float Width of the cell along x. dy : float Width of the cell along y. pintype : PinCellType How the pin cell should be split (along x, y, or only a quadrant). Returns ------- PinCell Pin cell suitable for the true MOC calculation. """ if len(self._fuel_ring_xs) != self.num_fuel_rings: raise RuntimeError("Fuel cross sections have not yet been built.") if self.gap is not None and self._gap_xs is None: raise RuntimeError("Gap cross section has not yet been built.") if self._clad_xs is None: raise RuntimeError("Clad cross section has not yet been built.") self._check_dx_dy(dx, dy, pintype) # Initialize the radii and cross section lists with the fuel info radii = [r for r in self._fuel_radii] xss = [xs for xs in self._fuel_ring_xs] # Add the gap (if present) if self._gap_xs is not None: radii.append(self.gap_radius) xss.append(self._gap_xs) # Add cladding radii.append(self.clad_radius) xss.append(self._clad_xs) # Add another ring of moderator if possible if pintype == PinCellType.Full and min(dx, dy) > 2.0 * self.clad_radius: radii.append(0.5 * min(dx, dy)) xss.append(moderator_xs) elif ( pintype in [PinCellType.XN, PinCellType.XP] and dx > self.clad_radius and dy > 2.0 * self.clad_radius ): radii.append(min(dx, 0.5 * dy)) xss.append(moderator_xs) elif ( pintype in [PinCellType.YN, PinCellType.YP] and dy > self.clad_radius and dx > 2.0 * self.clad_radius ): radii.append(min(0.5 * dx, dy)) xss.append(moderator_xs) elif dx > self.clad_radius and dy > self.clad_radius: radii.append(min(dx, dy)) xss.append(moderator_xs) # Add moderator to the end of materials xss.append(moderator_xs) # Create the cell object cell = PinCell(radii, xss, dx, dy, pintype) # Get the FSR IDs for the regions of interest cell_fsr_ids = cell.get_all_fsr_ids() # Number of angular divisions NA = 8 if pintype in [PinCellType.XN, PinCellType.XP, PinCellType.YN, PinCellType.YP]: NA = 4 elif pintype in [ PinCellType.I, PinCellType.II, PinCellType.III, PinCellType.IV, ]: NA = 2 I = 0 # Starting index for cell_fsr_inds # Go through all rings, and get FSR IDs for r in range(self.num_fuel_rings): for a in range(NA): self._fuel_ring_fsr_ids[r].append(cell_fsr_ids[I]) I += 1 # Get the FSRs for the gap, if present if self._gap_xs is not None: for a in range(NA): self._gap_fsr_ids.append(cell_fsr_ids[I]) I += 1 # Get the FSRs for the cladding for a in range(NA): self._clad_fsr_ids.append(cell_fsr_ids[I]) I += 1 # Everything else should be a moderator FSR self._mod_fsr_ids = list(cell_fsr_ids[I:]) return cell
[docs] def populate_fsr_indexes(self, moc: MOCDriver) -> None: """ Obtains the flat source region indexes for all of the flat source regions used in the full MOC calculations. Parameters ---------- moc : MOCDriver MOC simulation for the full calculations. """ self._fuel_ring_fsr_inds: List[List[int]] = [] for r in range(self.num_fuel_rings): self._fuel_ring_fsr_inds.append([]) self._gap_fsr_inds: List[int] = [] self._clad_fsr_inds: List[int] = [] self._mod_fsr_inds: List[int] = [] for r in range(self.num_fuel_rings): for id in self._fuel_ring_fsr_ids[r]: self._fuel_ring_fsr_inds[r].append(moc.get_fsr_indx(id, 0)) for id in self._gap_fsr_ids: self._gap_fsr_inds.append(moc.get_fsr_indx(id, 0)) for id in self._clad_fsr_ids: self._clad_fsr_inds.append(moc.get_fsr_indx(id, 0)) for id in self._mod_fsr_ids: self._mod_fsr_inds.append(moc.get_fsr_indx(id, 0))
[docs] def obtain_flux_spectra(self, moc: MOCDriver) -> None: """ Computes the average flux spectrum for each fuel ring from the MOC simulation. Each ring's flux spectrum is volume averaged. Parameters ---------- moc : MOCDriver MOC simulation for the full calculations. """ for r in range(self.num_fuel_rings): self._fuel_ring_flux_spectra[r] = moc.homogenize_flux_spectrum( self._fuel_ring_fsr_inds[r] )
[docs] def compute_pin_linear_power(self, ndl: NDLibrary): """ Computes the linear power density of the fuel pin based on the current flux spectra, in units of w / cm. Does not consider the partial pin geometry at the assembly level (i.e. a half pin in a quarter assembly). Parameters ---------- ndl : NDLibrary Nuclear data library for the fission energy release. Returns ------- float Linear power density in w / cm. """ power = 0.0 A = np.pi * self.fuel_radius**2.0 / self.num_fuel_rings for r in range(self.num_fuel_rings): mat = self._fuel_ring_materials[r][-1] flux = self._fuel_ring_flux_spectra[r] power += A * mat.compute_fission_power_density(flux, ndl) # Convert from MeV/cm/s to J/cm/s = w/cm power *= 1.602176634e-13 return power
[docs] def normalize_flux_spectrum(self, f) -> None: """ Applies a multiplicative factor to the flux spectra for the fuel rings. This permits normalizing the flux to a known assembly power. Parameters ---------- f : float Normalization factor. """ if f <= 0.0: raise ValueError("Normalization factor must be > 0.") for r in range(self.num_fuel_rings): self._fuel_ring_flux_spectra[r] *= f
[docs] def predict_depletion( self, chain: DepletionChain, ndl: NDLibrary, dt: float, dtm1: Optional[float] = None, ) -> None: """ Performs the predictor in the integration of the Bateman equation. If the argument for the previous time step is not provided, CE/LI will be used. Otherwise, CE/LI is used on the first depletion step, and LE/QI is used for all subsequent time steps. The predicted material compositions are appended to the materials lists. Parameters ---------- chain : DepletionChain Depletion chain to use for radioactive decay and transmutation. ndl : NDLibrary Nuclear data library. dt : float Durration of the time step in seconds. dtm1 : float, optional Durration of the previous time step in seconds. Default is None. """ if dt <= 0: raise ValueError("Predictor time step must be > 0.") # Do the prediction step for each fuel ring for r in range(self.num_fuel_rings): # Get the flux and initial material flux = self._fuel_ring_flux_spectra[r] mat = self._fuel_ring_materials[r][-1] # Use last available mat ! # Build depletion matrix for beginning of time step A0 = build_depletion_matrix(chain, mat, flux, ndl) # Save current matrix self._fuel_ring_current_dep_mats[r] = A0 # At this point, we can clear the xs data from the last material as # depletion matrix is now built. mat.clear_all_micro_xs_data() # Initialize an array with the initial target number densities N = np.zeros(A0.size) nuclides = A0.nuclides for i, nuclide in enumerate(nuclides): N[i] = mat.atom_density(nuclide) if self._fuel_ring_prev_dep_mats[r] is None or dtm1 is None: # Use CE/LI A0 *= dt # Do the matrix exponential A0.exponential_product(N) # Undo multiplication by time step on the matrix A0 /= dt else: # Use LE/QI Am1 = self._fuel_ring_prev_dep_mats[r] F1 = (-dt / (12.0 * dtm1)) * Am1 + ( (6.0 * dtm1 + dt) / (12.0 * dtm1) ) * A0 F1 *= dt F2 = (-5.0 * dt / (12.0 * dtm1)) * Am1 + ( (6.0 * dtm1 + 5.0 * dt) / (12.0 * dtm1) ) * A0 F2 *= dt # Do the matrix exponentials F1.exponential_product(N) F2.exponential_product(N) # Now we can build a new material composition new_mat_comp = MaterialComposition() for i, nuclide in enumerate(nuclides): if N[i] > 0.0: new_mat_comp.add_nuclide(nuclide, N[i]) # Make the new material new_mat = Material(new_mat_comp, mat.temperature, ndl) self._fuel_ring_materials[r].append(new_mat)
[docs] def correct_depletion( self, chain: DepletionChain, ndl: NDLibrary, dt: float, dtm1: Optional[float] = None, ) -> None: """ Performs the corrector in the integration of the Bateman equation. If the argument for the previous time step is not provided, CE/LI will be used. Otherwise, CE/LI is used on the first depletion step, and LE/QI is used for all subsequent time steps. The corrected material compositions replace the ones where were appended in the corrector step. Parameters ---------- chain : DepletionChain Depletion chain to use for radioactive decay and transmutation. ndl : NDLibrary Nuclear data library. dt : float Durration of the time step in seconds. dtm1 : float, optional Durration of the previous time step in seconds. Default is None. """ if dt <= 0: raise ValueError("Corrector time step must be > 0.") # Do the prediction step for each fuel ring for r in range(self.num_fuel_rings): # Get the flux and initial material flux = self._fuel_ring_flux_spectra[r] mat_pred = self._fuel_ring_materials[r][-1] # Use last available mat ! # Get depletion matrix for beginning of time step A0 = self._fuel_ring_current_dep_mats[r] # Build depletion matrix and multiply by time step Ap1 = build_depletion_matrix(chain, mat_pred, flux, ndl) # Initialize an array with the initial target number densities mat_old = self._fuel_ring_materials[r][-2] # Go 2 steps back !! N = np.zeros(Ap1.size) nuclides = Ap1.nuclides for i, nuclide in enumerate(nuclides): N[i] = mat_old.atom_density(nuclide) if self._fuel_ring_prev_dep_mats[r] is None or dtm1 is None: # Use CE/LI F1 = (5.0 * dt / 12.0) * A0 + (dt / 12.0) * Ap1 F2 = (dt / 12.0) * A0 + (5.0 * dt / 12.0) * Ap1 F1.exponential_product(N) F2.exponential_product(N) else: # Use LE/QI # Get previous depletion matrix Am1 = self._fuel_ring_prev_dep_mats[r] F3 = ( (-dt * dt / (12.0 * dtm1 * (dtm1 + dt))) * Am1 + ( (5.0 * dtm1 * dtm1 + 6.0 * dtm1 * dt + dt * dt) / (12.0 * dtm1 * (dtm1 + dt)) ) * A0 + (dtm1 / (12.0 * (dtm1 + dt))) * Ap1 ) F3 *= dt F4 = ( (-dt * dt / (12.0 * dtm1 * (dtm1 + dt))) * Am1 + ( (dtm1 * dtm1 + 2.0 * dtm1 * dt + dt * dt) / (12.0 * dtm1 * (dtm1 + dt)) ) * A0 + ((5.0 * dtm1 + 4.0 * dt) / (12.0 * (dtm1 + dt))) * Ap1 ) F4 *= dt F3.exponential_product(N) F4.exponential_product(N) # Now we can build a new material composition new_mat_comp = MaterialComposition() for i, nuclide in enumerate(nuclides): if N[i] > 0.0: new_mat_comp.add_nuclide(nuclide, N[i]) # Make the new material new_mat = Material(new_mat_comp, mat_pred.temperature, ndl) self._fuel_ring_materials[r][-1] = new_mat # Save the current matrix as previous matrix for next step ! self._fuel_ring_prev_dep_mats[r] = A0 self._fuel_ring_current_dep_mats[r] = None