Source code for scarabee.reseau.pwr_assembly

from .burnable_poison_rod import BurnablePoisonRod
from .fuel_pin import FuelPin
from .guide_tube import GuideTube
from .critical_leakage import CriticalLeakage
from .symmetry import Symmetry
from .. import PolarQuadrature
from ._ensleeve import (
    _ensleeve_quarter,
    _ensleeve_half_top,
    _ensleeve_half_right,
    _ensleeve_full,
)
from .equivalence_theory import compute_adf_cdf_from_cmfd, compute_adf_cdf_from_moc
from .._scarabee import (
    borated_water,
    Material,
    CrossSection,
    NDLibrary,
    DepletionChain,
    PinCellType,
    Vector,
    Direction,
    Cartesian2D,
    MOCDriver,
    CMFD,
    BoundaryCondition,
    SimulationMode,
    Legendre8,
    YamamotoTabuchi6,
    P1CriticalitySpectrum,
    B1CriticalitySpectrum,
    FundamentalModeCriticalitySpectrum,
    ADF,
    CDF,
    DiffusionCrossSection,
    DiffusionData,
    FormFactors,
    LeakageCorrections,
    set_logging_level,
    scarabee_log,
    LogLevel,
)
from typing import Optional, List, Tuple, Union
import copy
from threading import Thread
import numpy as np
from scipy.optimize import curve_fit


[docs]class PWRAssembly: """ A PWRAssembly instance is responsible for performing all the lattice calculations necessary to produce few-group cross sections for a single PWR assembly. Parameters ---------- shape : tuple of int The number of pin cells in the full assembly along x and y. pitch : float The spacing between fuel pins. ndl : NDLibrary Nuclear data library used for the calculation. cells : list of list of FuelPin or GuideTube All of the cells which describe the assembly geometry. Should be consistent with the symmetry argument. moderator : dict Parameters for defining the moderator. The boron concentration, temperature, and pressure will be used to call :py:func:`scarabee.borated_water`. Alternatively, the `material` key can be used to provide a user specified moderator material. Default is an empty dictionary (all default values). Acceptable keys are: :boron-ppm: Moderator boron concentration in parts per million. Default is 800 if not provided. (float) :temperature: Moderator temperature in Kelvin. Default is 570 if not provided. (float) :pressure: Moderator pressure in MPa. Default is 15.5 if not provided. (float) :legendre-order: Maximum Legendre order to load for the moderator's anisotropic scattering data. Default is 1 if not provided. (int) :material: User defined material which will be used directly. The scarabee.borated_water function will not be called. (scarabee.Material) symmetry : Symmetry Symmetry of the fuel assembly. Default is Symmetry.Quarter. independent_quadrant : bool If symmetry is Symmetry.Quarter and this attribute is true, the quarter assembly is treated as an independent assembly, with ADFs generated for all sides and CDFs generated for all corners. Form factors are only generated for the pins present in the quarter assembly. This improves the modeling of asymmetric burnable absorber loadings in the nodal calculation. Default value is False. linear_power : float Linear power density of the full assembly in kW/cm. This value should not be reduced due to symmetry. Default is 42. assembly_pitch : optional float Spacing between fuel assemblies. If None, assembly pitch is calculated from the shape and pitch. Default value is None. spacer_grid_width : optional float Width of the spacer grid material between pin cells. Default value is None. spacer_grid : optional Material Material defining the spacer grid between pin cells. grid_sleeve_width : optional float Width of the grid sleeve around the assembly. Default value is None. grid_sleeve : optional Material Material defining the grid sleeve around the assembly. Attributes ---------- shape : tuple of int The number of pin cells in the full assembly along x and y. pitch : float The spacing between fuel pins. symmetry : Symmetry Symmetry of the fuel assembly. independent_quadrant : bool If symmetry is Symmetry.Quarter and this attribute is true, the quarter assembly is treated as an independent assembly, with ADFs generated for all sides and CDFs generated for all corners. Form factors are only generated for the pins present in the quarter assembly. This improves the modeling of asymmetric burnable absorber loadings in the nodal calculation. assembly_pitch : float Spacing between fuel assemblies. fuel_volume_fraction : float Fraction of the assembly occupied by fuel. moderator_volume_fraction : float Fraction of the assembly occupied by moderator. linear_power : float Linear power density of the full assembly in kW/cm. initial_heavy_metal_linear_mass : float Initial linear density of heavy metal in the full assembly at the beginning of life in units of kg/cm. boron_ppm : float Moderator boron concentration in parts per million. moderator_temp : float Moderator temperature in Kelvin. moderator_pressure : float Moderator pressure in MPa. moderator_legendre_order : int Maximum Legendre order to load for the moderator's anisotropic scattering data. moderator : Material Material representing the assembly moderator. spacer_grid_width : optional float Width of the spacer grid material between pin cells. spacer_grid : optional Material Material defining the spacer grid between pin cells. grid_sleeve_width : optional float Width of the grid sleeve around the assembly. grid_sleeve : optional Material Material defining the grid sleeve around the assembly. dancoff_moc_track_spacing : float Spacing between tracks in the MOC calculations for determining Dancoff corrections. Default value is 0.05 cm. dancoff_moc_num_angles : int Number of azimuthal angles in the MOC calculations for determining Dancoff corrections. Default value is 32. dancoff_flux_tolerance : float Flux convergence tolerance for Dancoff correction calculations. Must be in range (0., 1.E-2). Default value is 1.E-5. moc_track_spacing : float Spacing between tracks in the assembly MOC calculations. Default value is 0.03 cm. moc_num_angles : int Number of azimuthal angles in the assembly MOC calculations. Default value is 32. moc_polar_quadrature : PolarQuadrature The polar quadrature used for the MOC calculations. Default is YamamotoTabuchi6 for isotropic calculations and Legendre8 for anisotropic calculations. flux_tolerance : float Flux convergence tolerance for assembly calculations. Must be in range (0., 1.E-2). Default value is 1.E-5. keff_tolerance : float Keff convergence tolerance for assembly calculations. Must be in range (0., 1.E-2). Default value is 1.E-5. anisotropic : bool If False, isotropic scattering with the transport correction is used in the assembly calculation. Otherwise, explicit anisotropic scattering is modeled. Default value is False. **If you wish to turn anisotropic scattering on, you must set this attribute before calling the solve method for the first time.** cmfd : bool If True, Coarse Mesh Finite Difference diffusion will be used to accelerate the assembly calculation. If True, a CMFD energy condensation scheme must be provided (see cmfd_condensation_scheme). Default value is True. **If you wish to turn CMFD off/on, you must set this attribute before calling the solve method for the first time.** cmfd_condensation_scheme : list of list of int Energy condensation scheme for the CMFD acceleration of the assembly calculation. condensation_scheme : list of list of int Energy condensation scheme to condense from the group structure of the library to the few-groups used in the core solver. prefer_moc_adf_cdf : bool If True, the MOC results will be used to generate the ADFs and CDFs, even if CMFD is being used. ADFs and CDFs that are generated in this manner are typically less accurate than those generated with the CMFD results. Default value is False. leakage_corrections : bool If True, the diffusion data contains few-group diffusion cross sections which DO NOT have the critical leakage model applied. Instead, a LeakageCorrections is created to update the few-group diffusion cross sections based on the in-situ leakage from the nodal diffusion solver. If False, the diffusion data does not have a LeakageCorrections instance and the few-group cross sections DO have the critical leakage model applied. Default is False. leakage_model : CriticalLeakage Model used to determine the critical leakage flux spectrum, also known as the fundamental mode. Default method is homogeneous P1. depletion_exposure_steps : optional ndarray 1D Numpy array of assembly burn-up exposure steps, in units of MWd/kg. Default is None. depletion_time_steps : optional ndarray 1D Numpy array of burn-up time steps, in units of days. Default is None. corrector_transport : bool If True, the corrector depletion step performs a transport calculation to obtain an updated solution for the flux based on the predictor nuclide concentrations, performing two transport calculations per time step. If False, the flux from the predictor transport calculation is used to perform the corrector step, performing only one transport calculation per time step. Default value is True. exposures : ndarray 1D Numpy array of the total assembly burn-up exposures at which material information is available, in units of MWd/kg. Default value is an empty array before solve has been called. times : ndarray 1D Numpy array of the total assembly burn-up times at which material information is available, in units of days. Default value is an empty array before solve has been called. keff : float or ndarray If depletion was not performed, this is a single float with keff for the infinite assembly. If depletion was performed, this is a 1D Numpy array for the values of keff at the tabulated burn-up exposures/times. Default value is 1 before solve has been called. depletion_data : optional DiffusionData or list of DiffusionData If a single assembly calculation is being performed without depletion, this attribute will the resulting DiffusionData instance based on the few-group condensation_scheme attribute. If a depletion calculation is performed, this attribute will be a list of DiffusionData instances, with one for each burn-up point. Before solve has been called, this attribute is None. """
[docs] def __init__( self, shape: Tuple[int, int], pitch: float, ndl: NDLibrary, cells: List[List[Union[FuelPin, GuideTube]]], moderator: dict = {}, symmetry: Symmetry = Symmetry.Quarter, independent_quadrant: bool = False, linear_power: float = 42.0, assembly_pitch: Optional[float] = None, spacer_grid_width: Optional[float] = None, spacer_grid: Optional[Material] = None, grid_sleeve_width: Optional[float] = None, grid_sleeve: Optional[Material] = None, ): self._ndl: NDLibrary = ndl self._chain: DepletionChain = self._ndl.depletion_chain self._symmetry: Symmetry = symmetry self._independent_quadrant: bool = independent_quadrant if self.symmetry != Symmetry.Quarter and self.independent_quadrant: raise RuntimeError( "Cannot have independent_quadrant set to True when not using quarter symmetry." ) if len(shape) != 2: raise ValueError("Shape must have 2 entries.") if shape[0] <= 0 or shape[1] <= 0: raise ValueError("Shape entries must be > 0.") if shape[0] != shape[1]: raise ValueError("Fuel assemblies must be square.") self._shape = shape # Compute the number of expected cells along each dimension [y][x] # based on the symmetry of the problem being solved. expx = self.shape[0] expy = self.shape[1] if self.symmetry == Symmetry.Half: expy = (expy // 2) + (expy % 2) elif self.symmetry == Symmetry.Quarter: expy = (expy // 2) + (expy % 2) expx = (expx // 2) + (expx % 2) self._simulated_shape = (expx, expy) self._initial_heavy_metal_linear_mass: float = 0.0 self._cells_set = False self._cells: List[List[Union[FuelPin, GuideTube]]] = [[]] self._set_cells(cells) if pitch <= 0.5: raise ValueError("Pitch must be > 0.5.") self._pitch = pitch # Compute assembly pitch self._assembly_pitch: float = self.shape[0] * self.pitch if assembly_pitch is not None: try: assembly_pitch = float(assembly_pitch) except: raise TypeError( "The assembly_pitch argument must be convertible to a float." ) if assembly_pitch <= 0.0: raise RuntimeError("Assembly pitch must be > 0.") elif assembly_pitch < self._assembly_pitch: raise RuntimeError( "Provided assembly pitch is smaller than that indicated by the pitch and shape." ) self._assembly_pitch = assembly_pitch # Can only compute the fuel volume fraction once the assembly pitch is known self._fuel_volume_fraction: float = 0.0 self._compute_fuel_volume_fraction() # Check the spacer grid parameters self._spacer_grid_width: Optional[float] = None self._spacer_grid: Optional[Material] = None if spacer_grid is None and spacer_grid_width is not None: raise RuntimeError("Spacer grid width is provided but material is not.") elif spacer_grid is not None and spacer_grid_width is None: raise RuntimeError("Spacer grid material is provided but width is not.") else: self._spacer_grid_width = spacer_grid_width self._spacer_grid = spacer_grid if self.spacer_grid_width is not None: if self._spacer_grid_width <= 0.0: raise ValueError("Spacer grid width must be > 0.") elif self._spacer_grid_width >= self._pitch: raise ValueError("Spacer grid width must be < pitch.") # Check the grid sleeve parameters self._grid_sleeve_width: Optional[float] = None self._grid_sleeve: Optional[Material] = None if grid_sleeve is None and grid_sleeve_width is not None: raise RuntimeError("Grid sleeve width is provided but material is not.") elif grid_sleeve is not None and grid_sleeve_width is None: raise RuntimeError("Grid sleeve material is provided but width is not.") else: self._grid_sleeve_width = grid_sleeve_width self._grid_sleeve = grid_sleeve if self.grid_sleeve_width is not None: if self._grid_sleeve_width <= 0.0: raise ValueError("Grid sleeve width must be > 0.") elif self._grid_sleeve_width >= 0.5 * ( self._assembly_pitch - self.shape[0] * self.pitch ): raise ValueError( "Grid sleeve width must be < the assembly gap width." ) # Can only be computed after spacer grid and grid sleeve processing self._moderator_volume_fraction: float = 0.0 self._compute_moderator_volume_fraction() # Get moderator parameters from the dictionary. self._boron_ppm = 800.0 self._moderator_temp = 570.0 self._moderator_pressure = 15.5 self._moderator_legendre_order = 1 for key in moderator: if key == "boron-ppm": self._boron_ppm = float(moderator[key]) if self._boron_ppm < 0.0: raise ValueError("Boron concentration must be >= 0.") elif key == "temperature": self._moderator_temp = float(moderator[key]) if self._moderator_temp <= 0.0: raise ValueError("Moderator temperature must be > 0.") elif key == "pressure": self._moderator_pressure = float(moderator[key]) if self._moderator_pressure <= 0.0: raise ValueError("Moderator pressure must be > 0.") elif key == "legendre-order": self._moderator_legendre_order = int(moderator[key]) if self._moderator_legendre_order < 1: raise ValueError("Moderator Legendre order must be >= 1.") elif key == "material": if isinstance(moderator[key], Material) == False: raise TypeError( "Provided moderator is not a scarabee.Material instance." ) self._moderator = moderator[key] else: raise KeyError(f'Unknown key "{key}" provided in moderator dictionary.') # If the moderator material wasn't directly provided, we make it from the info try: self._moderator except AttributeError: # Make material for borated water self._moderator: Material = borated_water( self.boron_ppm, self.moderator_temp, self.moderator_pressure, self._ndl ) self._moderator.name = f"Moderator ({self.boron_ppm} ppm boron)" self._moderator.max_legendre_order = self._moderator_legendre_order if linear_power <= 0.0: raise ValueError("Linear power must be > 0.") self._linear_power = linear_power # Set initial boundary conditions self._x_min_bc = BoundaryCondition.Periodic self._x_max_bc = BoundaryCondition.Periodic self._y_min_bc = BoundaryCondition.Periodic self._y_max_bc = BoundaryCondition.Periodic if self.symmetry != Symmetry.Full: self._y_min_bc = BoundaryCondition.Reflective self._y_max_bc = BoundaryCondition.Reflective if self.symmetry == Symmetry.Quarter: self._x_min_bc = BoundaryCondition.Reflective self._x_max_bc = BoundaryCondition.Reflective # ====================================================================== # DANCOFF CORRECTION CALCULATION DATA # ---------------------------------------------------------------------- # Make water xs for dancoff calculation self._moderator_dancoff_xs: CrossSection = CrossSection( np.array([self.moderator.potential_xs]), np.array([self.moderator.potential_xs]), np.array([[0.0]]), "Moderator", ) # Spacer grid and grid sleeve dancoff cross sections self._spacer_grid_dancoff_xs: Optional[CrossSection] = None self._grid_sleeve_dancoff_xs: Optional[CrossSection] = None if self.spacer_grid is not None: self._spacer_grid_dancoff_xs: CrossSection = CrossSection( np.array([self.spacer_grid.potential_xs]), np.array([self.spacer_grid.potential_xs]), np.array([[0.0]]), "Spacer Grid", ) if self.grid_sleeve is not None: self._grid_sleeve_dancoff_xs: CrossSection = CrossSection( np.array([self.grid_sleeve.potential_xs]), np.array([self.grid_sleeve.potential_xs]), np.array([[0.0]]), "Grid Sleeve", ) # Isolated cell geometry for Dancoff correction calculations self._isolated_dancoff_cells = [] self._isolated_dancoff_mocs = [] # Full geometry for Dancoff correction calculations self._full_dancoff_cells = [] self._full_dancoff_geom = None self._full_dancoff_moc = None # Dancoff correction parameters self._dancoff_isolation_scale = 10.0 self._dancoff_moc_track_spacing = 0.05 self._dancoff_moc_num_angles = 32 self._dancoff_flux_tolerance = 1.0e-5 self._fuel_dancoff_corrections = np.zeros( (self._simulated_shape[1], self._simulated_shape[0]) ) self._clad_dancoff_corrections = np.zeros( (self._simulated_shape[1], self._simulated_shape[0]) ) # ====================================================================== # TRANSPORT CALCULATION DATA # ---------------------------------------------------------------------- # Make water xs for transport calculation self._moderator_xs: CrossSection = self.moderator.dilution_xs( self.moderator.size * [1.0e10], self._ndl ) # Spacer grid and grid sleeve cross sections self._spacer_grid_xs: Optional[CrossSection] = None self._grid_sleeve_xs: Optional[CrossSection] = None if self.spacer_grid is not None: self._spacer_grid_xs: CrossSection = self.spacer_grid.dilution_xs( self.spacer_grid.size * [1.0e10], self._ndl ) if self.grid_sleeve is not None: self._grid_sleeve_xs: CrossSection = self.grid_sleeve.dilution_xs( self.grid_sleeve.size * [1.0e10], self._ndl ) self._moc_track_spacing: float = 0.03 self._moc_num_angles: int = 64 # Default polar quadrature chosen later based on _anisotropic self._moc_polar_quadrature: Optional[PolarQuadrature] = None self._flux_tolerance: float = 1.0e-5 self._keff_tolerance: float = 1.0e-5 self._anisotropic: bool = False self._cmfd: bool = True self._prefer_moc_adf_cdf: bool = False self._asmbly_cells = [] self._asmbly_geom: Optional[Cartesian2D] = None self._asmbly_moc: Optional[MOCDriver] = None self._leakage_corrections: bool = False self._leakage_model: CriticalLeakage = CriticalLeakage.P1 self._infinite_flux_spectrum = ( None # To reset to infinite spectrum in MOC driver ) # Depletion time steps in MWd/kg and depletion time steps in days. # Initially starts as None (should be provided by user) self._depletion_exposure_steps: Optional[np.ndarray] = None self._depletion_time_steps: Optional[np.ndarray] = None # Arrays for the depletion exposures (MWd/kg) and times (days) self._exposures: np.ndarray = np.array([]) self._times: np.ndarray = np.array([]) # Indicates if 2 (True) or 1 (False) transport calculation should be # performed per time step. self._corrector_transport: bool = True # Either a single value or list of values (for each depletion step) self._keff: Union[float, List[float]] = 1.0 # Condensation scheme to make few-group cross sections self._condensation_scheme: Optional[List[List[int]]] = ( self._ndl.condensation_scheme ) # Condensation scheme to used for CMFD acceleration self._cmfd_condensation_scheme: Optional[List[List[int]]] = ( self._ndl.cmfd_condensation_scheme ) # If a single assembly calculation is performed, this attribute will # contain a single DiffusionData instance. If a depletion calculation # is performed, a DiffusionData instance will be generated for each # burn-up step. self._diffusion_data: Optional[Union[DiffusionData, List[DiffusionData]]] = None self._form_factors: Optional[Union[FormFactors, List[FormFactors]]] = None
@property def shape(self) -> Tuple[int, int]: return self._shape @property def pitch(self) -> float: return self._pitch @property def symmetry(self) -> Symmetry: return self._symmetry @property def independent_quadrant(self) -> bool: return self._independent_quadrant @property def assembly_pitch(self) -> float: return self._assembly_pitch @property def fuel_volume_fraction(self) -> float: return self._fuel_volume_fraction @property def moderator_volume_fraction(self) -> float: return self._moderator_volume_fraction @property def spacer_grid_width(self) -> Optional[float]: return self._spacer_grid_width @property def spacer_grid(self) -> Optional[Material]: return self._spacer_grid @property def grid_sleeve_width(self) -> Optional[float]: return self._grid_sleeve_width @property def grid_sleeve(self) -> Optional[Material]: return self._grid_sleeve @property def linear_power(self) -> float: return self._linear_power @property def initial_heavy_metal_linear_mass(self) -> float: return self._initial_heavy_metal_linear_mass @property def boron_ppm(self) -> float: return self._boron_ppm @property def moderator_temp(self) -> float: return self._moderator_temp @property def moderator_pressure(self) -> float: return self._moderator_pressure @property def moderator_legendre_order(self) -> int: return self._moderator_legendre_order @property def moderator(self) -> Material: return self._moderator @property def dancoff_moc_track_spacing(self) -> float: return self._dancoff_moc_track_spacing @dancoff_moc_track_spacing.setter def dancoff_moc_track_spacing(self, dts: float) -> None: if dts <= 0.0 or dts > 0.1: raise ValueError("Dancoff track spacing must be in range (0, 0.1].") self._dancoff_moc_track_spacing = dts @property def dancoff_moc_num_angles(self) -> int: return self._dancoff_moc_num_angles @dancoff_moc_num_angles.setter def dancoff_moc_num_angles(self, dna: int) -> None: if dna % 4 != 0: raise ValueError( "Number of angles for Dancoff correction calculation must be a multiple of 4." ) if dna < 4: raise ValueError( "Number of angles for Dancoff correction calculation must be > 4." ) self._dancoff_moc_num_angles = int(dna) @property def dancoff_flux_tolerance(self) -> float: return self._dancoff_flux_tolerance @dancoff_flux_tolerance.setter def dancoff_flux_tolerance(self, tol: float) -> None: try: tol = float(tol) except: raise TypeError( "Dancoff flux tolerance must be a floating point value in (0,1.E-2)." ) if tol <= 0.0 or tol >= 1.0e-2: raise ValueError("Dancoff flux tolerance must be in (0,1.E-2).") self._dancoff_flux_tolerance = tol @property def moc_track_spacing(self) -> float: return self._moc_track_spacing @moc_track_spacing.setter def moc_track_spacing(self, dts: float) -> None: if dts <= 0.0 or dts > 0.1: raise ValueError("Track spacing must be in range (0, 0.1].") self._moc_track_spacing = dts @property def moc_num_angles(self) -> int: return self._moc_num_angles @moc_num_angles.setter def moc_num_angles(self, dna: int) -> None: if dna % 4 != 0: raise ValueError( "Number of angles for MOC calculation must be a multiple of 4." ) if dna < 4: raise ValueError("Number of angles for MOC calculation must be > 4.") self._moc_num_angles = int(dna) @property def moc_polar_quadrature(self) -> Optional[PolarQuadrature]: return self._moc_polar_quadrature @moc_polar_quadrature.setter def moc_polar_quadrature(self, pq: PolarQuadrature) -> None: self._moc_polar_quadrature = pq @property def flux_tolerance(self) -> float: return self._flux_tolerance @flux_tolerance.setter def flux_tolerance(self, tol: float) -> None: try: tol = float(tol) except: raise TypeError( "Flux tolerance must be a floating point value in (0,1.E-2)." ) if tol <= 0.0 or tol >= 1.0e-2: raise ValueError("Flux tolerance must be in (0,1.E-2).") self._flux_tolerance = tol @property def keff_tolerance(self) -> float: return self._keff_tolerance @keff_tolerance.setter def keff_tolerance(self, tol: float) -> None: try: tol = float(tol) except: raise TypeError( "Keff tolerance must be a floating point value in (0,1.E-2)." ) if tol <= 0.0 or tol >= 1.0e-2: raise ValueError("Keff tolerance must be in (0,1.E-2).") self._keff_tolerance = tol @property def anisotropic(self) -> bool: return self._anisotropic @anisotropic.setter def anisotropic(self, aniso: bool) -> None: self._anisotropic = aniso @property def cmfd(self) -> bool: return self._cmfd @cmfd.setter def cmfd(self, cmfd: bool) -> None: self._cmfd = cmfd @property def cells(self) -> List[List[Union[FuelPin, GuideTube]]]: return self._cells @property def leakage_corrections(self) -> bool: return self._leakage_corrections @leakage_corrections.setter def leakage_corrections(self, lc: bool) -> None: self._leakage_corrections = lc @property def leakage_model(self) -> CriticalLeakage: return self._leakage_model @leakage_model.setter def leakage_model(self, clm: CriticalLeakage) -> None: self._leakage_model = clm @property def depletion_exposure_steps(self) -> Optional[np.ndarray]: return self._depletion_exposure_steps @depletion_exposure_steps.setter def depletion_exposure_steps(self, steps: Optional[np.ndarray]) -> None: if steps is None: self._depletion_exposure_steps = None self._depletion_time_steps = None return if not isinstance(steps, np.ndarray): raise TypeError("Depletion exposure steps must be a 1D Numpy array.") if steps.ndim != 1: raise ValueError("Depletion exposure steps must be a 1D Numpy array.") if steps.size == 0: raise ValueError( "Depletion exposure steps must have at least one entry. Use None for no depletion." ) for step in steps: if step <= 0.0: raise ValueError("Depletion exposure steps must be > 0.") elif step > 5.0: raise ValueError("Depletion exposure steps should be <= 5 MWd/kg.") self._depletion_exposure_steps = steps self._depletion_time_steps = self._depletion_exposure_steps.copy() self._depletion_time_steps *= ( 1.0e3 * (1.0 / self.linear_power) * self.initial_heavy_metal_linear_mass ) @property def depletion_time_steps(self) -> Optional[np.ndarray]: return self._depletion_time_steps @depletion_time_steps.setter def depletion_time_steps(self, steps: Optional[np.ndarray]) -> None: if steps is None: self._depletion_time_steps = None self._depletion_exposure_steps = None return if not isinstance(steps, np.ndarray): raise TypeError("Depletion time steps must be a 1D Numpy array.") if steps.ndim != 1: raise ValueError("Depletion time steps must be a 1D Numpy array.") if steps.size == 0: raise ValueError( "Depletion time steps must have at least one entry. Use None for no depletion." ) for step in steps: if step <= 0.0: raise ValueError("Depletion time steps must be > 0.") elif step > 100: raise ValueError("Depletion time steps should be <= 100 days.") self._depletion_time_steps = steps self._depletion_exposure_steps = self._depletion_time_steps.copy() self._depletion_exposure_steps /= ( 1.0e3 * (1.0 / self.linear_power) * self.initial_heavy_metal_linear_mass ) @property def exposures(self) -> np.ndarray: return self._exposures @property def times(self) -> np.ndarray: return self._times @property def corrector_transport(self) -> bool: return self._corrector_transport @corrector_transport.setter def corrector_transport(self, val: bool) -> None: self._corrector_transport = val @property def keff(self) -> Union[float, List[float]]: return self._keff @property def condensation_scheme(self) -> Optional[List[List[int]]]: return self._condensation_scheme @condensation_scheme.setter def condensation_scheme(self, cs: Optional[List[List[int]]]) -> None: if cs is None: self._condensation_scheme = None return if not isinstance(cs, list): raise TypeError("Condensation scheme must be a list of lists of ints.") if len(cs) == 0: raise TypeError("Condensation scheme must have at least one group.") for G in range(len(cs)): if not isinstance(cs[G], list): raise TypeError("Condensation scheme must be a list of lists of ints.") if len(cs[G]) != 2: raise TypeError( "Each entry in condensation scheme must have 2 entries." ) try: cs[G][0] = int(cs[G][0]) cs[G][1] = int(cs[G][1]) except: raise TypeError( f"Microgroup indices for macrogroup {G} are not convertible to ints." ) if cs[G][1] < cs[G][0]: raise ValueError( f"The microgroup indices in macrogroup {G} are not ordered." ) if G == 0 and cs[G][0] != 0: raise ValueError( "The first microgroup index of the 0 macrogroup must be 0." ) elif G == len(cs) - 1 and cs[G][1] != self._ndl.ngroups - 1: NG = self._ndl.ngroups - 1 raise ValueError( f"The last microgroup index of the last macrogroup must be {NG}." ) if G > 0: if cs[G][0] != cs[G - 1][1] + 1: raise ValueError("The condensation scheme is not continuous") self._condensation_scheme = copy.deepcopy(cs) @property def prefer_moc_adf_cdf(self) -> bool: return self._prefer_moc_adf_cdf @prefer_moc_adf_cdf.setter def prefer_moc_adf_cdf(self, value: bool) -> None: self._prefer_moc_adf_cdf = value @property def cmfd_condensation_scheme(self) -> Optional[List[List[int]]]: return self._cmfd_condensation_scheme @cmfd_condensation_scheme.setter def cmfd_condensation_scheme(self, cs: Optional[List[List[int]]]) -> None: if cs is None: self._cmfd_condensation_scheme = None return if not isinstance(cs, list): raise TypeError("CMFD Condensation scheme must be a list of lists of ints.") if len(cs) == 0: raise TypeError("CMFD Condensation scheme must have at least one group.") for G in range(len(cs)): if not isinstance(cs[G], list): raise TypeError( "CMFD Condensation scheme must be a list of lists of ints." ) if len(cs[G]) != 2: raise TypeError( "Each entry in CMFD condensation scheme must have 2 entries." ) try: cs[G][0] = int(cs[G][0]) cs[G][1] = int(cs[G][1]) except: raise TypeError( f"Microgroup indices for macrogroup {G} are not convertible to ints." ) if cs[G][1] < cs[G][0]: raise ValueError( f"The microgroup indices in macrogroup {G} are not ordered." ) if G == 0 and cs[G][0] != 0: raise ValueError( "The first microgroup index of the 0 macrogroup must be 0." ) elif G == len(cs) - 1 and cs[G][1] != self._ndl.ngroups - 1: NG = self._ndl.ngroups - 1 raise ValueError( f"The last microgroup index of the last macrogroup must be {NG}." ) if G > 0: if cs[G][0] != cs[G - 1][1] + 1: raise ValueError("The CMFD condensation scheme is not continuous") self._cmfd_condensation_scheme = copy.deepcopy(cs) @property def diffusion_data(self) -> Optional[Union[DiffusionData, List[DiffusionData]]]: return self._diffusion_data @property def form_factors(self) -> Optional[Union[FormFactors, List[FormFactors]]]: return self._form_factors def _set_cells(self, cells: List[List[Union[FuelPin, GuideTube]]]) -> None: if len(cells) != self._simulated_shape[1]: raise ValueError( "Shape along y of cells list does not agree with assembly shape and symmetry." ) for j in range(len(cells)): if len(cells[j]) != self._simulated_shape[0]: raise ValueError( "Shape along x of cells list does not agree with assembly shape and symmetry." ) # Make deep copies of all cells provided by the user. This makes sure # that each duplicate FuelPin or GuideTube instance is unique. self._cells = [] self._cells_set = False self._initial_heavy_metal_linear_mass = 0.0 for j in range(len(cells)): self._cells.append([]) for i in range(len(cells[j])): self._cells[-1].append(copy.deepcopy(cells[j][i])) if isinstance(cells[j][i], FuelPin): lfm = cells[j][i].initial_fissionable_linear_mass # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): lfm *= 0.25 # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): lfm *= 0.5 # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): lfm *= 0.5 self._initial_heavy_metal_linear_mass += lfm self._cells_set = True if self.symmetry == Symmetry.Half: self._initial_heavy_metal_linear_mass *= 2.0 elif self.symmetry == Symmetry.Quarter: self._initial_heavy_metal_linear_mass *= 4.0 # Convert HM mass from g to kg self._initial_heavy_metal_linear_mass *= 1.0e-3 def _compute_fuel_volume_fraction(self) -> None: """ Computes the fraction of the assembly which is fuel. """ sum_volume = 0.0 for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if not isinstance(cell, FuelPin): continue pin_volume = np.pi * cell.fuel_radius * cell.fuel_radius # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): pin_volume *= 0.25 # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): pin_volume *= 0.5 # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): pin_volume *= 0.5 sum_volume += pin_volume if self.symmetry == Symmetry.Quarter: sum_volume *= 4.0 elif self.symmetry == Symmetry.Half: sum_volume *= 2.0 self._fuel_volume_fraction = sum_volume / ( self.assembly_pitch * self.assembly_pitch ) def _compute_moderator_volume_fraction(self) -> None: """ Computes the fraction of the assembly which is moderator. """ sum_volume = 0.0 pin_cell_volume = self.pitch**2.0 if self.spacer_grid_width is not None: pin_cell_volume = (self.pitch - 2.0 * self.spacer_grid_width) ** 2.0 for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] # Start with volume of moderator in a pin cell cell_mod_volume = pin_cell_volume if isinstance(cell, FuelPin): # Remove volume of pin cell_mod_volume -= np.pi * cell.clad_radius * cell.clad_radius else: # Guide tube # First, remove volume of the tube cell_mod_volume -= np.pi * ( cell.outer_radius * cell.outer_radius - cell.inner_radius * cell.inner_radius ) # Remove volume of the burnable poison rod (if present) if isinstance(cell.fill, BurnablePoisonRod): cell_mod_volume -= ( np.pi * cell.fill.outer_clad_radius * cell.fill.outer_clad_radius ) # Add center volume back if the center is moderator ! if cell.fill.center is not None: cell_mod_volume += ( np.pi * cell.fill.center_radius * cell.fill.center_radius ) # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): cell_mod_volume *= 0.25 # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): cell_mod_volume *= 0.5 # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): cell_mod_volume *= 0.5 sum_volume += cell_mod_volume if self.symmetry == Symmetry.Quarter: sum_volume *= 4.0 elif self.symmetry == Symmetry.Half: sum_volume *= 2.0 # Add volume for the gap around the assembly if self.grid_sleeve_width is None: sum_volume += self.assembly_pitch**2.0 - (self.shape[0] * self.pitch) ** 2.0 else: sum_volume += ( self.assembly_pitch**2.0 - (self.shape[0] * self.pitch + 2.0 * self.grid_sleeve_width) ** 2.0 ) self._moderator_volume_fraction = sum_volume / ( self.assembly_pitch * self.assembly_pitch ) # ========================================================================== # Interrogation Methods
[docs] def get_average_fuel_nuclide_density(self, t: int, nuclide: str) -> float: """ Computes the average density of a nuclide within all the fuel pellets in the assembly 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 all the fuel pellets in units of atoms per barn-cm. """ sum_density = 0.0 sum_volume = 0.0 for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if not isinstance(cell, FuelPin): continue pin_avg_density = cell.get_average_fuel_nuclide_density(t, nuclide) pin_volume = np.pi * cell.fuel_radius * cell.fuel_radius # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): pin_volume *= 0.25 # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): pin_volume *= 0.5 # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): pin_volume *= 0.5 sum_density += pin_volume * pin_avg_density sum_volume += pin_volume return sum_density / sum_volume
# ========================================================================== # Dancoff Correction Related Methods def _init_dancoff_components(self) -> None: scarabee_log( LogLevel.Info, "Initializing Dancoff correction calculation components." ) set_logging_level(LogLevel.Warning) self._init_isolated_dancoff_components() self._init_full_dancoff_components() self._save_dancoff_fsr_indexes() set_logging_level(LogLevel.Info) def _init_isolated_dancoff_components(self) -> None: # Isolated pitch iso_pitch = self._dancoff_isolation_scale * self.pitch for j in range(len(self.cells)): self._isolated_dancoff_cells.append([]) self._isolated_dancoff_mocs.append([]) for i in range(len(self.cells[j])): cell = None geom = None moc = None x_min_bc = BoundaryCondition.Vacuum x_max_bc = BoundaryCondition.Vacuum y_min_bc = BoundaryCondition.Vacuum y_max_bc = BoundaryCondition.Vacuum # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, 0.5 * iso_pitch, 0.5 * iso_pitch, PinCellType.I, True, ) geom = Cartesian2D([0.5 * iso_pitch], [0.5 * iso_pitch]) geom.set_tiles([cell]) x_min_bc = BoundaryCondition.Reflective y_min_bc = BoundaryCondition.Reflective # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, 0.5 * iso_pitch, iso_pitch, PinCellType.XP, True, ) geom = Cartesian2D([0.5 * iso_pitch], [iso_pitch]) geom.set_tiles([cell]) x_min_bc = BoundaryCondition.Reflective # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, iso_pitch, 0.5 * iso_pitch, PinCellType.YP, True, ) geom = Cartesian2D([iso_pitch], [0.5 * iso_pitch]) geom.set_tiles([cell]) y_min_bc = BoundaryCondition.Reflective # Otherwise, we just make the full cell else: cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, iso_pitch, iso_pitch, PinCellType.Full, True, ) geom = Cartesian2D([iso_pitch], [iso_pitch]) geom.set_tiles([cell]) # Save to cells self._isolated_dancoff_cells[-1].append(cell) # Make the MOCDriver moc = MOCDriver(geom) moc.sim_mode = SimulationMode.FixedSource moc.x_min_bc = x_min_bc moc.x_max_bc = x_max_bc moc.y_min_bc = y_min_bc moc.y_max_bc = y_max_bc # Generate tracks in serial as each call will run with threads moc.generate_tracks( self._dancoff_moc_num_angles, self._dancoff_moc_track_spacing, YamamotoTabuchi6(), ) # Save the MOC self._isolated_dancoff_mocs[-1].append(moc) def _init_full_dancoff_components(self) -> None: pitch = self.pitch if self.spacer_grid_width is not None: pitch -= 2.0 * self.spacer_grid_width # Get all the cells for j in range(len(self.cells)): for i in range(len(self.cells[j])): # Get the geometry bit from the cell cell = None # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, 0.5 * pitch, 0.5 * pitch, PinCellType.I, False, ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_quarter( cell, pitch, self.spacer_grid_width, self._spacer_grid_dancoff_xs, ) # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, 0.5 * pitch, pitch, PinCellType.XP, False, ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_half_right( cell, pitch, self.spacer_grid_width, self._spacer_grid_dancoff_xs, ) # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, pitch, 0.5 * pitch, PinCellType.YP, False, ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_half_top( cell, pitch, self.spacer_grid_width, self._spacer_grid_dancoff_xs, ) # Otherwise, we just make the full cell else: cell = self.cells[j][i].make_dancoff_moc_cell( self._moderator_dancoff_xs, pitch, pitch, PinCellType.Full, False, ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_full( cell, pitch, self.spacer_grid_width, self._spacer_grid_dancoff_xs, ) # Save to cells self._full_dancoff_cells.append(cell) # Construct the Cartesian2D geometry dx = self._simulated_shape[0] * [self.pitch] dy = self._simulated_shape[1] * [self.pitch] if self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1: dy[0] *= 0.5 if self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1: dx[0] *= 0.5 self._full_dancoff_geom = Cartesian2D(dx, dy) self._full_dancoff_geom.set_tiles(self._full_dancoff_cells) # Add gap if needed gap_width = 0.5 * (self.assembly_pitch - self.shape[0] * self.pitch) if self.grid_sleeve_width is not None: gap_width -= self.grid_sleeve_width if gap_width > 0.0: pins_geom = self._full_dancoff_geom if self.symmetry == Symmetry.Quarter: if self.grid_sleeve_width is not None: pins_geom, _ = _ensleeve_quarter( pins_geom, self.pitch, self._grid_sleeve_width, self._grid_sleeve_dancoff_xs, ) self._full_dancoff_geom, _ = _ensleeve_quarter( pins_geom, self.pitch, gap_width, self._moderator_dancoff_xs ) elif self.symmetry == Symmetry.Half: if self.grid_sleeve_width is not None: pins_geom, _ = _ensleeve_half_top( pins_geom, self.pitch, self._grid_sleeve_width, self._grid_sleeve_dancoff_xs, ) self._full_dancoff_geom, _ = _ensleeve_half_top( pins_geom, self.pitch, gap_width, self._moderator_dancoff_xs ) else: if self.grid_sleeve_width is not None: pins_geom, _ = _ensleeve_full( pins_geom, self.pitch, self._grid_sleeve_width, self._grid_sleeve_dancoff_xs, ) self._full_dancoff_geom, _ = _ensleeve_full( pins_geom, self.pitch, gap_width, self._moderator_dancoff_xs ) # Construct the MOC self._full_dancoff_moc = MOCDriver(self._full_dancoff_geom) self._full_dancoff_moc.sim_mode = SimulationMode.FixedSource self._full_dancoff_moc.x_min_bc = self._x_min_bc self._full_dancoff_moc.x_max_bc = self._x_max_bc self._full_dancoff_moc.y_min_bc = self._y_min_bc self._full_dancoff_moc.y_max_bc = self._y_max_bc self._full_dancoff_moc.generate_tracks( self._dancoff_moc_num_angles, self._dancoff_moc_track_spacing, YamamotoTabuchi6(), ) def _save_dancoff_fsr_indexes(self) -> None: for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] isomoc = self._isolated_dancoff_mocs[j][i] cell.populate_dancoff_fsr_indexes(isomoc, self._full_dancoff_moc) def _dancoff_components_initialized(self) -> bool: if self._full_dancoff_moc is None: return False return True
[docs] def set_dancoff_moderator_xs(self) -> None: """ Updates the moderator cross section for all Dancoff correction calculations. """ self._moderator_dancoff_xs.set( CrossSection( np.array([self.moderator.potential_xs]), np.array([self.moderator.potential_xs]), np.array([[0.0]]), "Moderator", ) )
[docs] def set_dancoff_spacer_grid_sleeve_xs(self) -> None: """ Updates the spacer grid and grid sleeve cross sections for all Dancoff correction calculations. """ if self.spacer_grid is not None: self._spacer_grid_dancoff_xs.set( CrossSection( np.array([self.spacer_grid.potential_xs]), np.array([self.spacer_grid.potential_xs]), np.array([[0.0]]), "Spacer Grid", ) ) if self.grid_sleeve is not None: self._grid_sleeve_dancoff_xs.set( CrossSection( np.array([self.grid_sleeve.potential_xs]), np.array([self.grid_sleeve.potential_xs]), np.array([[0.0]]), "Grid Sleeve", ) )
[docs] def compute_fuel_dancoff_corrections(self) -> None: """ Recomputes all Dancoff corrections for the fuel regions in the problem, using the most recent material definitions. All fuel is shelf-shielded together, regardless of wether or not is is UO2 or MOX. """ scarabee_log(LogLevel.Info, "Computing Dancoff corrections for the fuel.") set_logging_level(LogLevel.Warning) if not self._dancoff_components_initialized(): raise RuntimeError( "Dancoff calculation components have not been initialized." ) # Set the xs and sources for all cells for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] isomoc = self._isolated_dancoff_mocs[j][i] isomoc.flux_tolerance = self.dancoff_flux_tolerance cell.set_xs_for_fuel_dancoff_calculation() cell.set_isolated_dancoff_fuel_sources(isomoc, self.moderator) cell.set_full_dancoff_fuel_sources( self._full_dancoff_moc, self.moderator ) self._full_dancoff_moc.flux_tolerance = self.dancoff_flux_tolerance # Solve all the MOCs in parallel threads = [] for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if isinstance(cell, FuelPin): isomoc = self._isolated_dancoff_mocs[j][i] threads.append(Thread(target=isomoc.solve)) threads[-1].start() threads.append(Thread(target=self._full_dancoff_moc.solve)) threads[-1].start() for t in threads: t.join() # Go through and let each cell compute the Dancoff correction if it holds # a fuel pin. for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] isomoc = self._isolated_dancoff_mocs[j][i] if isinstance(cell, FuelPin): C = cell.compute_fuel_dancoff_correction( isomoc, self._full_dancoff_moc ) self._fuel_dancoff_corrections[j, i] = C set_logging_level(LogLevel.Info)
[docs] def compute_clad_dancoff_corrections(self) -> None: """ Recomputes all Dancoff corrections for the fuel pin cladding regions in the problem, using the most recent material definitions. All cladding is shelf-shielded together. """ scarabee_log(LogLevel.Info, "Computing Dancoff corrections for the cladding.") set_logging_level(LogLevel.Warning) if self._full_dancoff_moc is None: raise RuntimeError( "Dancoff calculation components have not been initialized." ) # Set the xs and sources for all cells for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] isomoc = self._isolated_dancoff_mocs[j][i] isomoc.flux_tolerance = self.dancoff_flux_tolerance cell.set_xs_for_clad_dancoff_calculation(self._ndl) cell.set_isolated_dancoff_clad_sources( isomoc, self.moderator, self._ndl ) cell.set_full_dancoff_clad_sources( self._full_dancoff_moc, self.moderator, self._ndl ) self._full_dancoff_moc.flux_tolerance = self.dancoff_flux_tolerance # Solve all the MOCs in parallel threads = [] for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] isomoc = self._isolated_dancoff_mocs[j][i] threads.append(Thread(target=isomoc.solve)) threads[-1].start() threads.append(Thread(target=self._full_dancoff_moc.solve)) threads[-1].start() for t in threads: t.join() # Go through and let each cell compute the Dancoff correction if it holds # a fuel pin. for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] isomoc = self._isolated_dancoff_mocs[j][i] C = cell.compute_clad_dancoff_correction(isomoc, self._full_dancoff_moc) self._clad_dancoff_corrections[j, i] = C set_logging_level(LogLevel.Info)
[docs] def apply_dancoff_corrections(self) -> None: """ Appends all fuel and cladding Dancoff corrections to the appropriate cell. """ for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if isinstance(cell, FuelPin): cell.append_fuel_dancoff_correction( self._fuel_dancoff_corrections[j, i] ) cell.append_clad_dancoff_correction( self._clad_dancoff_corrections[j, i] )
[docs] def self_shield_and_xs_update(self) -> None: """ Computes a new set of Dancoff corrections for the fuel and the cladding. After, these are applied to all the cells in the problem. """ if not self._dancoff_components_initialized(): self._init_dancoff_components() # Update the Dancoff cross sections held by the assembly self.set_dancoff_moderator_xs() self.set_dancoff_spacer_grid_sleeve_xs() # Compute Dancoff corrections self.compute_fuel_dancoff_corrections() self.compute_clad_dancoff_corrections() self.apply_dancoff_corrections()
# ========================================================================== # Transport Calculation Related Methods def _init_moc(self) -> None: pitch = self.pitch if self.spacer_grid_width is not None: pitch -= 2.0 * self.spacer_grid_width # Get all the cells for j in range(len(self.cells)): for i in range(len(self.cells[j])): # Get the geometry bit from the cell cell = None # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): cell = self.cells[j][i].make_moc_cell( self._moderator_xs, 0.5 * pitch, 0.5 * pitch, PinCellType.I, ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_quarter( cell, pitch, self.spacer_grid_width, self._spacer_grid_xs ) # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): cell = self.cells[j][i].make_moc_cell( self._moderator_xs, 0.5 * pitch, pitch, PinCellType.XP ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_half_right( cell, pitch, self.spacer_grid_width, self._spacer_grid_xs ) # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): cell = self.cells[j][i].make_moc_cell( self._moderator_xs, pitch, 0.5 * pitch, PinCellType.YP ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_half_top( cell, pitch, self.spacer_grid_width, self._spacer_grid_xs ) # Otherwise, we just make the full cell else: cell = self.cells[j][i].make_moc_cell( self._moderator_xs, pitch, pitch, PinCellType.Full ) if self.spacer_grid_width is not None: cell, _ = _ensleeve_full( cell, pitch, self.spacer_grid_width, self._spacer_grid_xs ) # Save to cells self._asmbly_cells.append(cell) # Construct the Cartesian2D geometry dx = self._simulated_shape[0] * [self.pitch] dy = self._simulated_shape[1] * [self.pitch] if self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1: dy[0] *= 0.5 if self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1: dx[0] *= 0.5 self._asmbly_geom = Cartesian2D(dx, dy) self._asmbly_geom.set_tiles(self._asmbly_cells) # Add gap if needed gap_width = 0.5 * (self.assembly_pitch - self.shape[0] * self.pitch) if self.grid_sleeve_width is not None: gap_width -= self.grid_sleeve_width if gap_width > 0.0: pins_geom = self._asmbly_geom if self.symmetry == Symmetry.Quarter: if self.grid_sleeve_width is not None: pins_geom, _ = _ensleeve_quarter( pins_geom, self.pitch, self._grid_sleeve_width, self._grid_sleeve_xs, ) self._asmbly_geom, _ = _ensleeve_quarter( pins_geom, self.pitch, gap_width, self._moderator_xs ) elif self.symmetry == Symmetry.Half: if self.grid_sleeve_width is not None: pins_geom, _ = _ensleeve_half_top( pins_geom, self.pitch, self._grid_sleeve_width, self._grid_sleeve_xs, ) self._asmbly_geom, _ = _ensleeve_half_top( pins_geom, self.pitch, gap_width, self._moderator_xs ) else: if self.grid_sleeve_width is not None: pins_geom, _ = _ensleeve_full( pins_geom, self.pitch, self._grid_sleeve_width, self._grid_sleeve_xs, ) self._asmbly_geom, _ = _ensleeve_full( pins_geom, self.pitch, gap_width, self._moderator_xs ) # Construct the MOC self._asmbly_moc = MOCDriver(self._asmbly_geom, anisotropic=self.anisotropic) self._asmbly_moc.x_min_bc = self._x_min_bc self._asmbly_moc.x_max_bc = self._x_max_bc self._asmbly_moc.y_min_bc = self._y_min_bc self._asmbly_moc.y_max_bc = self._y_max_bc # Apply CMFD if turned on if self.cmfd and self.cmfd_condensation_scheme is None: raise RuntimeError( "CMFD is enabled but no CMFD condensation scheme has been provided." ) elif self.cmfd: extra_width = 0.5 * (self.assembly_pitch - self.shape[0] * self.pitch) dx_cmfd = self._simulated_shape[0] * [self.pitch] dy_cmfd = self._simulated_shape[1] * [self.pitch] if self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1: dy_cmfd[0] *= 0.5 if self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1: dx_cmfd[0] *= 0.5 if self.symmetry == Symmetry.Full: dx_cmfd[0] += extra_width dx_cmfd[-1] += extra_width dy_cmfd[0] += extra_width dy_cmfd[-1] += extra_width elif self.symmetry == Symmetry.Half: dx_cmfd[0] += extra_width dx_cmfd[-1] += extra_width dy_cmfd[-1] += extra_width else: dx_cmfd[-1] += extra_width dy_cmfd[-1] += extra_width self._asmbly_moc.cmfd = CMFD( dx_cmfd, dy_cmfd, self.cmfd_condensation_scheme ) if self.anisotropic and self.moc_polar_quadrature is None: self.moc_polar_quadrature = Legendre8() elif self.moc_polar_quadrature is None: self.moc_polar_quadrature = YamamotoTabuchi6() # Trace tracks self._asmbly_moc.generate_tracks( self.moc_num_angles, self.moc_track_spacing, self.moc_polar_quadrature, ) self._save_fsr_indexes() def _save_fsr_indexes(self) -> None: for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] cell.populate_fsr_indexes(self._asmbly_moc)
[docs] def plot(self) -> None: """ Launches the graphical geometry plotter for the assembly calculation. """ if self._asmbly_moc is None: raise RuntimeError( "Cannot launch plotter. MOCDriver has not been initialized." ) self._asmbly_moc.plot()
[docs] def set_moderator_xs(self) -> None: """ Updates the moderator cross section for transport calculations. """ self._moderator_xs.set( self.moderator.dilution_xs(self.moderator.size * [1.0e10], self._ndl) )
[docs] def set_spacer_grid_sleeve_xs(self) -> None: """ Updates the spacer grid and grid sleeve cross sections for transport calculations. """ if self.spacer_grid is not None: self._spacer_grid_xs.set( self.spacer_grid.dilution_xs( self.spacer_grid.size * [1.0e10], self._ndl ) ) if self.grid_sleeve is not None: self._grid_sleeve_xs.set( self.grid_sleeve.dilution_xs( self.grid_sleeve.size * [1.0e10], self._ndl ) )
[docs] def recompute_all_xs(self) -> None: """ Computes and applies all cross sections using the most recent material information and Dancoff corrections. """ self.recompute_all_fuel_xs() self.recompute_all_gap_xs() self.recompute_all_clad_xs() self.recompute_all_guide_tube_fill_xs() self.set_moderator_xs() self.set_spacer_grid_sleeve_xs()
[docs] def recompute_all_self_shielded_xs(self) -> None: """ Computes and applies all fuel and caldding cross sections using the most recent material information and Dancoff corrections. """ self.recompute_all_fuel_xs() self.recompute_all_clad_xs()
[docs] def recompute_all_fuel_xs(self) -> None: """ Computes and applies all fuel cross sections using the most recent material information and Dancoff corrections. """ for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if isinstance(cell, FuelPin): cell.set_fuel_xs_for_depletion_step(-1, self._ndl)
[docs] def recompute_all_clad_xs(self) -> None: """ Computes and applies all cladding cross sections using the most recent material information and Dancoff corrections. """ for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] cell.set_clad_xs_for_depletion_step(-1, self._ndl)
[docs] def recompute_all_gap_xs(self) -> None: """ Computes and applies all gap cross sections using the most recent material information. """ for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if isinstance(cell, FuelPin): cell.set_gap_xs(self._ndl)
[docs] def recompute_all_guide_tube_fill_xs(self) -> None: """ Computes and applies all cross sections for the fill objects of guide tubes. These could be for burnable poison rods or control rods. """ for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if isinstance(cell, GuideTube): cell.set_fill_xs_for_depletion_step(-1, self._ndl)
[docs] def apply_leakage_model(self, scilent: bool = False) -> None: """ Applied the critical leakage model to the assembly, modifying the flux in the MOC simulation directly. Parameters ---------- scilent : bool If True, nothing is written to the log. If False, the type of spectrum calculation, as well as kinf and the buckling are logged. Default value is False. """ # If no leakage, just return if self.leakage_model == CriticalLeakage.NoLeakage: return if not scilent: scarabee_log(LogLevel.Info, "") self._infinite_flux_spectrum = self._asmbly_moc.homogenize_flux_spectrum() homogenized_moc = self._asmbly_moc.homogenize() if self.leakage_model == CriticalLeakage.P1: if not scilent: scarabee_log( LogLevel.Info, "Performing P1 criticality spectrum calculation" ) critical_spectrum = P1CriticalitySpectrum(homogenized_moc) elif self.leakage_model == CriticalLeakage.B1: if not scilent: scarabee_log( LogLevel.Info, "Performing B1 criticality spectrum calculation" ) critical_spectrum = B1CriticalitySpectrum(homogenized_moc) else: if not scilent: scarabee_log( LogLevel.Info, "Performing fundamental mode criticality spectrum calculation", ) critical_spectrum = FundamentalModeCriticalitySpectrum(homogenized_moc) self._asmbly_moc.apply_criticality_spectrum(critical_spectrum.flux) if not scilent: scarabee_log( LogLevel.Info, "Kinf : {:.5f}".format(critical_spectrum.k_inf) ) scarabee_log( LogLevel.Info, "Buckling: {:.5f}".format(critical_spectrum.buckling) )
[docs] def apply_infinite_spectrum(self) -> None: """ Undoes the critical flux spectrum adjustment to the MOCDriver. This permits subsequent transport calcualtions to converge much faster. """ if self.leakage_model == CriticalLeakage.NoLeakage: return if self._infinite_flux_spectrum is not None and self._asmbly_moc is not None: self._asmbly_moc.apply_criticality_spectrum(self._infinite_flux_spectrum)
[docs] def obtain_flux_spectra(self) -> None: """ Computes the average flux spectrum for material regions whcih are depleted. This includes each fuel ring in fuel pins and the poison in burnable poison rods. """ for j in range(len(self.cells)): for i in range(len(self.cells[j])): self.cells[j][i].obtain_flux_spectra(self._asmbly_moc)
[docs] def normalize_flux_to_power(self) -> None: """ Normalizes the flux spectra based on the specified linear power density for the assembly. It assumes that all power comes from fission. """ assembly_linear_power = 0.0 for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if not isinstance(cell, FuelPin): continue pin_linear_power = cell.compute_pin_linear_power(self._ndl) # First check for quarter symmetry and being corner pin if ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 and i == 0 ): pin_linear_power *= 0.25 # Next, check for being on the side with a half pin in quarter symmetry elif ( self.symmetry == Symmetry.Quarter and self.shape[0] % 2 == 1 and i == 0 ): pin_linear_power *= 0.5 # Next, check for being on the bottom row with a half pin elif ( self.symmetry != Symmetry.Full and self.shape[1] % 2 == 1 and j == self._simulated_shape[1] - 1 ): pin_linear_power *= 0.5 assembly_linear_power += pin_linear_power if self.symmetry == Symmetry.Half: assembly_linear_power *= 2.0 elif self.symmetry == Symmetry.Quarter: assembly_linear_power *= 4.0 # Compute normalization factor. # Multiply by 10^3 to go from kW to W on linear power f = 1.0e3 * self.linear_power / assembly_linear_power # Normalize flux spectra for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i].normalize_flux_spectrum(f)
def _compute_form_factors(self) -> FormFactors: """ Computes the one group pin power form factors for the full assembly. Returns ------- FormFactors A FormFactors object for the pin power reconstruction. First index is y (from high to low) and second index is x (from low to high). """ if self.symmetry == Symmetry.Quarter and self.independent_quadrant: ff = np.zeros((self._simulated_shape[1], self._simulated_shape[0])) for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if not isinstance(cell, FuelPin): continue # Pin Power ff[j, i] = cell.compute_pin_linear_power(self._ndl) mean_ff = np.mean(ff) ff /= mean_ff # Create the widths arrays x_widths = np.zeros(len(self.cells[0])) y_widths = np.zeros(len(self.cells)) x_widths[:] = self.pitch y_widths[:] = self.pitch if self.shape[0] % 2 == 1 and self.shape[1] % 2 == 1: x_widths[0] *= 0.5 y_widths[0] *= 0.5 gap_width = 0.5 * (self.assembly_pitch - self.shape[0] * self.pitch) if gap_width > 0.0: x_widths[-1] += gap_width y_widths[-1] += gap_width return FormFactors(ff, x_widths, y_widths) else: ff = np.zeros((self.shape[1], self.shape[0])) for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] if not isinstance(cell, FuelPin): continue # Pin Power pp = cell.compute_pin_linear_power(self._ndl) if self.symmetry == Symmetry.Full: ff[j, i] = pp elif self.symmetry == Symmetry.Half: ff[j, i] = pp ff[-(j + 1), i] = pp elif self.symmetry == Symmetry.Quarter: ox = self.shape[1] // 2 ff[j, i + ox] = pp ff[j, -(i + ox + 1)] = pp ff[-(j + 1), i + ox] = pp ff[-(j + 1), -(i + ox + 1)] = pp mean_ff = np.mean(ff) ff /= mean_ff # Create the widths arrays x_widths = np.zeros(self.shape[1]) y_widths = np.zeros(self.shape[0]) x_widths[:] = self.pitch y_widths[:] = self.pitch gap_width = 0.5 * (self.assembly_pitch - self.shape[0] * self.pitch) if gap_width > 0.0: x_widths[0] += gap_width y_widths[0] += gap_width x_widths[-1] += gap_width y_widths[-1] += gap_width return FormFactors(ff, x_widths, y_widths) def _compute_few_group_xs(self) -> DiffusionCrossSection: """ Computes the few-group diffusion cross sections for the problem. Returns ------- DiffusionCrossSection Few-group diffusion cross sections for the assembly. Raises ------ RuntimeError If the condensation_scheme attribute is not set. """ if self.condensation_scheme is None: raise RuntimeError("Energy condensation scheme not set.") # According to Smith, one should do energy condensation on the # diffusion coefficients, and not on the transport cross sections which # one could then use to make diffusion coefficients [1]. This is in # contradiction to Lattice Physics Computations which states that # either method is acceptable [2]. In light of these comments, I have # chosen to go with Smith's recommendation of performing energy # condensation on the diffusion coefficients. # Get homogenized cross sections for assembly in MOC group structure homog_xs = self._asmbly_moc.homogenize() # Obtain the flux spectrum for condensation if self.leakage_corrections or self.leakage_model == CriticalLeakage.NoLeakage: flux_spectrum = self._asmbly_moc.homogenize_flux_spectrum() elif self.leakage_model == CriticalLeakage.P1: flux_spectrum = P1CriticalitySpectrum(homog_xs).flux elif self.leakage_model == CriticalLeakage.B1: flux_spectrum = B1CriticalitySpectrum(homog_xs).flux else: flux_spectrum = FundamentalModeCriticalitySpectrum(homog_xs).flux # Convert xs to diffusion xs, then condense diff_xs = homog_xs.diffusion_xs() return diff_xs.condense(self.condensation_scheme, flux_spectrum) def _compute_leakage_corrections(self) -> LeakageCorrections: """ Computes the coefficients used by the nodal diffusion solver to update the few-group cross sections based on the in-situ leakage in the full core simulation. """ if self.condensation_scheme is None: raise RuntimeError("Energy condensation scheme not set.") if self.leakage_model == CriticalLeakage.NoLeakage: raise RuntimeError( "Cannot generate leakage corrections without a leakage model." ) # Fitting function def fnc(xdata, C): return C * xdata # Number of few-groups NG = len(self.condensation_scheme) # initialize the leakage correction object lc = LeakageCorrections(NG) # Get list of bucklings, and generate all few-group cross section sets NB = 11 # Must be odd ! B2s = np.linspace(start=-0.01, stop=0.01, num=NB) xss = [] homog_xs = self._asmbly_moc.homogenize() diff_xs = homog_xs.diffusion_xs() for B2 in B2s: if self.leakage_model == CriticalLeakage.P1: flux_spectrum = P1CriticalitySpectrum(homog_xs, B2).flux elif self.leakage_model == CriticalLeakage.B1: flux_spectrum = B1CriticalitySpectrum(homog_xs, B2).flux else: flux_spectrum = FundamentalModeCriticalitySpectrum(homog_xs, B2).flux xss.append(diff_xs.condense(self.condensation_scheme, flux_spectrum)) xs_ref = xss[int(NB / 2)] D = np.zeros(NB) Ea = np.zeros(NB) Ef = np.zeros(NB) vEf = np.zeros(NB) Es = np.zeros(NB) for G_in in range(NG): D.fill(0.0) Ea.fill(0.0) Ef.fill(0.0) vEf.fill(0.0) # For this group, fill all the buckling dependent data for b in range(NB): D[b] = xss[b].D(G_in) Ea[b] = xss[b].Ea(G_in) Ef[b] = xss[b].Ef(G_in) vEf[b] = xss[b].vEf(G_in) # Compute the leakage to loss ratio LRr = (D * B2s) / (xs_ref.Er(G_in)) # Compute fractional changes f_D = (D - xs_ref.D(G_in)) / xs_ref.D(G_in) f_Ea = (Ea - xs_ref.Ea(G_in)) / xs_ref.Ea(G_in) f_Ef = (Ef - xs_ref.Ef(G_in)) / xs_ref.Ef(G_in) f_vEf = (vEf - xs_ref.vEf(G_in)) / xs_ref.vEf(G_in) # Fit leakage correction coefficients C_D, _ = curve_fit(fnc, LRr, f_D) C_Ea, _ = curve_fit(fnc, LRr, f_Ea) C_Ef, _ = curve_fit(fnc, LRr, f_Ef) C_vEf, _ = curve_fit(fnc, LRr, f_vEf) # Store computed values lc.set_D(G_in, C_D) lc.set_Ea(G_in, C_Ea) lc.set_Ef(G_in, C_Ef) lc.set_vEf(G_in, C_vEf) # Do same for each down-scattering transition for G_out in range(G_in + 1, NG): Es.fill(0.0) for b in range(NB): Es[b] = xss[b].Es(G_in, G_out) f_Es = (Es - xs_ref.Es(G_in, G_out)) / xs_ref.Es(G_in, G_out) C_Es, _ = curve_fit(fnc, LRr, f_Es) lc.set_Es(G_in, G_out, C_Es) return lc def _compute_diffusion_data_and_form_factors( self, ) -> Tuple[DiffusionData, FormFactors]: """ Computes the nodal diffusion data for the assembly along with the form factors for pin power reconstruction. If self.leakage_corrections is True, the coefficients used to update the few-group cross sections in the nodal diffusion solver based on the in-situ leakge will be computed. In this case, the few-group cross sections DO NOT have the leakage model applied. Otherwise, the leakage correction coefficients are not generated and the few-group cross sections are leakage corrected. Returns ------- DiffusionData Few-group diffusion cross sections and discontinuity factors. FormFactors Form factors to be used for pin power reconstruction. """ if self.condensation_scheme is None: raise RuntimeError("Energy condensation scheme not set.") diff_xs = self._compute_few_group_xs() ff = self._compute_form_factors() if ( self.cmfd and self.cmfd_condensation_scheme is not None and not self.prefer_moc_adf_cdf ): # If we are using CMFD, we should use the currents to generate the # ADFs and CDFs, as it is far more accurate, as explained by Smith [1]. adf, cdf = compute_adf_cdf_from_cmfd( self._asmbly_moc, self.condensation_scheme, self.symmetry, self.independent_quadrant, ) else: if not self.prefer_moc_adf_cdf: mssg = "CMFD is not on. Accurate discontinuity factors cannot be computed without CMFD." scarabee_log(LogLevel.Warning, mssg) adf, cdf = compute_adf_cdf_from_moc( self._asmbly_moc, self.condensation_scheme, self.symmetry, self.independent_quadrant, ) diff_data = DiffusionData(diff_xs, adf, cdf) if self.leakage_corrections and self.leakage_model == CriticalLeakage.NoLeakage: scarabee_log( LogLevel.Warning, "Leakage corrections were requested, but no critical leakage model is provided.", ) scarabee_log( LogLevel.Warning, "Leakage correction coefficients WILL NOT be generated.", ) elif ( self.leakage_corrections and self.leakage_model != CriticalLeakage.NoLeakage ): diff_data.leakage_corrections = self._compute_leakage_corrections() return diff_data, ff def _run_assembly_calculation( self, self_shield: bool, apply_dancoff_corrections: bool = False, transport: bool = True, ) -> None: """ Runs a single MOC calculation, applies critical leakage model, obtains the flux spectra for each fuel region, and then normalize the flux based on the linear assembly power. Self-shielding can be performed if desired. If not, the last computed Dancoff corrections can be applied to all fuel pins (to run a time step without explicit self-shielding step, using previously known Dancoff corrections). Paramters --------- self_shield : bool If True, self-shielding is performed for the fuel and cladding. apply_dancoff_corrections : bool, default False If self_shield is False and this option is True, the previously obtained Dancoff corrections are applied to all cells. transport : bool, default True If True, the MOC calculation is performed. Otherwise, the MOC calculation is not performed, but the leakage correction and flux normalization are. """ if self_shield: # If we want self-shielding, do that stuff self.self_shield_and_xs_update() elif apply_dancoff_corrections: # Sets dancoff corrections, even if self-shielding wasn't performed self.apply_dancoff_corrections() self.recompute_all_xs() if self._asmbly_moc is None: self._init_moc() self._asmbly_moc.flux_tolerance = self.flux_tolerance self._asmbly_moc.keff_tolerance = self.keff_tolerance if transport: set_logging_level(LogLevel.Warning) self._asmbly_moc.solve() set_logging_level(LogLevel.Info) scarabee_log(LogLevel.Info, "") scarabee_log(LogLevel.Info, "Kinf: {:.5f}".format(self._asmbly_moc.keff)) self.apply_leakage_model(scilent=self.leakage_corrections) self.obtain_flux_spectra() self.normalize_flux_to_power() self.apply_infinite_spectrum() def _predict_depletion(self, dt: float, dtm1: Optional[float]) -> None: # Do all depletions in parallel threads = [] for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] threads.append( Thread( target=cell.predict_depletion, args=(self._chain, self._ndl, dt, dtm1), ) ) threads[-1].start() for t in threads: t.join() def _correct_depletion(self, dt: float, dtm1: Optional[float]) -> None: # Do all depletions in parallel threads = [] for j in range(len(self.cells)): for i in range(len(self.cells[j])): cell = self.cells[j][i] threads.append( Thread( target=cell.correct_depletion, args=(self._chain, self._ndl, dt, dtm1), ) ) threads[-1].start() for t in threads: t.join() def _run_depletion_steps(self) -> None: if self._chain is None: raise RuntimeError( "No depletion chain is present. Cannot run depletion calculation." ) self._keff = np.zeros(self.depletion_exposure_steps.size + 1) self._exposures = np.zeros(self.depletion_exposure_steps.size + 1) self._times = np.zeros(self.depletion_exposure_steps.size + 1) self._diffusion_data = [] self._form_factors = [] for t, dt in enumerate(self.depletion_time_steps): if t > 0: self._exposures[t] = ( self._exposures[t - 1] + self.depletion_exposure_steps[t - 1] ) self._times[t] = self._times[t - 1] + self.depletion_time_steps[t - 1] # Don't write separating line at first iteration scarabee_log(LogLevel.Info, 60 * "-") scarabee_log(LogLevel.Info, "Running Time Step {:}".format(t)) scarabee_log( LogLevel.Info, "Exposure: {:.3E} MWd/kg".format(self._exposures[t]) ) scarabee_log(LogLevel.Info, "Time : {:.3E} days".format(self._times[t])) scarabee_log(LogLevel.Info, "") # Convert days to seconds dt_sec = dt * 60.0 * 60.0 * 24.0 dtm1_sec = None if t > 0: dtm1_sec = self.depletion_time_steps[t - 1] * 60.0 * 60.0 * 24.0 scarabee_log(LogLevel.Info, "Predictor:") # Run initial calcualtion for this time step self._run_assembly_calculation(True) scarabee_log(LogLevel.Info, "") self._keff[t] = self._asmbly_moc.keff dd, ff = self._compute_diffusion_data_and_form_factors() self._diffusion_data.append(dd) self._form_factors.append(ff) # Predic isotopes at midpoint of step self._predict_depletion(dt_sec, dtm1_sec) scarabee_log(LogLevel.Info, "Corrector:") # Run the a new transport calcualtion to get rates self._run_assembly_calculation(False, transport=self.corrector_transport) # Do correction step for isotopes self._correct_depletion(dt_sec, dtm1_sec) scarabee_log(LogLevel.Info, "") # Run last step at the end to get keff for our final material compositions scarabee_log(LogLevel.Info, "") scarabee_log(LogLevel.Info, 60 * "-") self._exposures[-1] = self._exposures[-2] + self.depletion_exposure_steps[-1] self._times[-1] = self._times[-2] + dt scarabee_log( LogLevel.Info, "Running Time Step {:}".format(self._times.size - 1) ) scarabee_log( LogLevel.Info, "Exposure: {:.3E} MWd/kg".format(self._exposures[-1]) ) scarabee_log(LogLevel.Info, "Time : {:.3E} days".format(self._times[-1])) scarabee_log(LogLevel.Info, "") self._run_assembly_calculation(True) self._keff[-1] = self._asmbly_moc.keff dd, ff = self._compute_diffusion_data_and_form_factors() self._diffusion_data.append(dd) self._form_factors.append(ff) scarabee_log(LogLevel.Info, "")
[docs] def solve(self) -> None: """ Solves the assembly problem. If depletion_exposure_steps or depletion_time_steps are None, then a single k-eigenvalue problem will be run. Otherwise, the depletion calculation will be performed. """ if self.depletion_exposure_steps is None: # Single one-off calulcation self._run_assembly_calculation(True) self._keff = self._asmbly_moc.keff self._diffusion_data, self._form_factors = ( self._compute_diffusion_data_and_form_factors() ) else: # Run depletion steps self._run_depletion_steps()
# REFERENCES # # [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. # # [2] D. Knott and A. Yamamoto, "Lattice Physics Computations" in # Handbook of Nuclear Engineering, 2010, p 1226.