TWIGL 2D reactor: multigroup Neutron Diffusion

This notebook implements the 2D TWIGL reactor, based on the paper Comparison of Alternating- Direction Time-Differencing Methods with Other Implicit Methods for the Solution of the Neutron Group-Diffusion Equations (L. A. Hageman and J. B. Yasinsky, 1968).

[1]:
from dolfinx.io import gmshio
import gmsh
from mpi4py import MPI
from IPython.display import clear_output

import numpy as np
import ufl
from dolfinx.fem import (Function)

import matplotlib.pyplot as plt
from matplotlib import cm

import warnings
warnings.filterwarnings("ignore")

import sys

mesh_path      = '../../../mesh/'
benchmark_path = '../../../BenchmarkData/'

sys.path.append('../../../models/fenicsx')

Preamble

The geometry and the main physical parameters will be assigned.

Mesh Import

The geometry and the mesh are imported from “TWIGL2D.msh”, generated with GMSH.

[2]:
gdim = 2
model_rank = 0
mesh_comm = MPI.COMM_WORLD

mesh_factor = 1

# Initialize the gmsh module
gmsh.initialize()

# Load the .geo file
gmsh.merge(mesh_path+'TWIGL2D.geo')
gmsh.model.geo.synchronize()

gmsh.option.setNumber("Mesh.MeshSizeFactor", mesh_factor)

gmsh.model.mesh.generate(gdim)
gmsh.model.mesh.optimize("Netgen")
clear_output()

# Domain
domain, ct, ft = gmshio.model_to_mesh(gmsh.model, comm = mesh_comm, rank = model_rank, gdim = gdim )
gmsh.finalize()

domain1_marker = 10
domain2_marker = 20
domain3_marker = 30

boundary_marker = 1

tdim = domain.topology.dim
fdim = tdim - 1

ds = ufl.Measure("ds", domain=domain, subdomain_data=ft)
dx = ufl.Measure("dx", domain=domain)

domain.topology.create_connectivity(fdim, tdim)

Define parameter functions on the different regions

Since there are 3 subdomains in \(\Omega\) (i.e., fuel-1, fuel-2, fuel-3) the values of the parameters changes according to the region, therefore proper functions should be defined.

[3]:
regions = [domain1_marker, domain2_marker, domain3_marker]

neutronics_param = dict()

neutronics_param['Energy Groups'] = 2

neutronics_param['D'] = [np.array([1.4, 1.4, 1.3]),
                         np.array([0.4, 0.4, 0.5])]
neutronics_param['xs_a'] = [np.array([0.01, 0.01, 0.008]),
                            np.array([0.15, 0.15, 0.05])]
neutronics_param['nu_xs_f'] = [np.array([0.007, 0.007, 0.003]),
                               np.array([0.2, 0.2, 0.06])]
neutronics_param['chi'] = [np.array([1,1,1]),
                           np.array([0,0,0])]
neutronics_param['B2z'] = [np.array([0,0,0]),
                           np.array([0,0,0])]
neutronics_param['xs_s'] = [[np.array([0,0,0]), np.array([0.01, 0.01, 0.01])],
                            [np.array([0,0,0]), np.array([0,0,0])]]

nu_value = 2.43
Ef = 1
reactor_power = 1

Solution of the eigenvalue problem

The MG diffusion equation is discretised using the Finite Element Method, and its eigenvalue formulation is solved through the standard inverse-power method.

[4]:
from neutronics.neutr_diff import steady_neutron_diff

neutr_steady_problem = steady_neutron_diff(domain, ct, ft, neutronics_param, regions, boundary_marker)
neutr_steady_problem.assembleForm()

# Solving the steady problem
phi_ss, k_eff = neutr_steady_problem.solve(power = reactor_power, Ef=Ef, nu = nu_value,
                                           LL = 10, maxIter = 500, verbose=True)
    Iter 010 | k_eff: 0.912975 | Rel Error: 1.046e-04
    Iter 020 | k_eff: 0.913202 | Rel Error: 4.514e-06
    Iter 030 | k_eff: 0.913214 | Rel Error: 3.149e-07
    Iter 040 | k_eff: 0.913215 | Rel Error: 2.327e-08
    Iter 050 | k_eff: 0.913215 | Rel Error: 1.730e-09
    Iter 060 | k_eff: 0.913215 | Rel Error: 1.287e-10
    Neutronics converged with 061 iter | k_eff: 0.91321502 | rho: -9503.24 pcm | Rel Error: 9.923e-11
ld: warning: duplicate -rpath '/Users/stefanoriva/miniconda3/envs/mp/lib' ignored
ld: warning: duplicate -rpath '/Users/stefanoriva/miniconda3/envs/mp/lib' ignored

Post-processing

The solution of the eigenvalue problem is compared with reference data.

Let us compare the \(k_{eff}\) with the benchmark value

[5]:
print('Computed  k_eff = {:.5f}'.format(k_eff))
print('Benchmark k_eff = {:.5f}'.format(0.91321))
print('Relative error  = {:.5f}'.format(np.abs(k_eff - 0.91321) / 0.91321 * 1e5)+' pcm')
Computed  k_eff = 0.91322
Benchmark k_eff = 0.91321
Relative error  = 0.55020 pcm

Let us plot the solution against benchmark data along the diagonal \(x=y\) and along the \(x\)-axis \(y=0\).

[6]:
from plotting import extract_cells

Nhplot = 1000
xMax = 80
x_line = np.linspace(0, xMax + 1e-20, Nhplot)
points = np.zeros((3, Nhplot))
points[0] = x_line
xPlot_0, cells_0 = extract_cells(domain, points)
points[1] = x_line
xPlot_1, cells_1 = extract_cells(domain, points)

fluxFigure = plt.figure( figsize = (8,6) )

plt.subplot(2,2,1)
plt.plot(xPlot_0[:,0], phi_ss[0].eval(xPlot_0, cells_0) / max(phi_ss[0].eval(xPlot_0, cells_0)), 'r', label = r'dolfinx')

plt.ylabel(r"$\phi_1$ at $y=0$",fontsize=20)
plt.grid(which='major',linestyle='-')
plt.grid(which='minor',linestyle='--')

plt.subplot(2,2,2)
plt.plot(xPlot_0[:,0], phi_ss[1].eval(xPlot_0, cells_0) / max(phi_ss[1].eval(xPlot_0, cells_0)), 'b', label = r'dolfinx')

plt.ylabel(r"$\phi_2$ at $y=0$",fontsize=20)
plt.grid(which='major',linestyle='-')
plt.grid(which='minor',linestyle='--')

plt.subplot(2,2,3)
plt.plot(xPlot_1[:,0], phi_ss[0].eval(xPlot_1, cells_1) / max(phi_ss[0].eval(xPlot_1, cells_1)), 'r', label = r'dolfinx')

plt.xlabel(r"$x\,[cm]$",fontsize=20)
plt.ylabel(r"$\phi_1$ at $y=x$",fontsize=20)
plt.grid(which='major',linestyle='-')
plt.grid(which='minor',linestyle='--')

plt.subplot(2,2,4)
plt.plot(xPlot_1[:,0], phi_ss[1].eval(xPlot_1, cells_1) / max(phi_ss[1].eval(xPlot_1, cells_1)), 'b', label = r'dolfinx')

plt.xlabel(r"$x\,[cm]$",fontsize=20)
plt.ylabel(r"$\phi_2$ at $y=x$",fontsize=20)
plt.grid(which='major',linestyle='-')
plt.grid(which='minor',linestyle='--')

plt.tight_layout()
../../../_images/Tutorials_fenicsx_neutronics_02_TWIGL2D_neutronics_ss_13_0.png

Let us know display the fluxes contour plots.

[7]:
from plotting import PlotScalar

PlotScalar(phi_ss[0], show=True, varname='Fast Flux', resolution=[720, 480], clim=[0, 0.04])
PlotScalar(phi_ss[1], show=True, varname='Thermal Flux', resolution=[720, 480], clim=[0, 0.0065])
../../../_images/Tutorials_fenicsx_neutronics_02_TWIGL2D_neutronics_ss_15_0.png
../../../_images/Tutorials_fenicsx_neutronics_02_TWIGL2D_neutronics_ss_15_1.png
[20]:
import pyvista as pv
from plotting import get_scalar_grid

def subPlot_flux(fast: Function, thermal: Function, time = None,
                 filename: str = None, clim = None,
                 cmap1 = cm.viridis, cmap2 = cm.magma, resolution = [1400, 600]):

    plotter = pv.Plotter(shape=(1,2), off_screen=False, border=False, window_size=resolution)

    lab_fontsize = 20
    title_fontsize = 25
    zoom = 1.1

    dict_cb = dict(title = ' ', width = 0.76,
                    title_font_size=title_fontsize,
                    label_font_size=lab_fontsize,
                    color='k',
                    position_x=0.12, position_y=0.89,
                    shadow=True)

    clim_scale = .01

    ################### Fast Flux ###################
    plotter.subplot(0,0)
    if clim is None:
        clim1 = [0,  max(fast.x.array) * (1+clim_scale)]
    dict_cb['title'] = 'Fast Flux'
    plotter.add_mesh(get_scalar_grid(fast, varname='phi1'), cmap = cmap1, clim = clim1, show_edges=False, scalar_bar_args=dict_cb)
    plotter.view_xy()
    plotter.camera.zoom(zoom)

    ################### Thermal Flux ###################
    plotter.subplot(0,1)
    if clim is None:
        clim2 = [0,  max(thermal.x.array) * (1+clim_scale)]
    dict_cb['title'] = 'Thermal Flux'
    plotter.add_mesh(get_scalar_grid(thermal, varname='phi2'), cmap = cmap2, clim = clim2, show_edges=False, scalar_bar_args=dict_cb)
    plotter.view_xy()
    plotter.camera.zoom(zoom)


    ###### Save figure ######

    plotter.set_background('white', top='white')
    plotter.screenshot(filename+'.png', transparent_background = True,  window_size=resolution)
    plotter.close()
[21]:
subPlot_flux(phi_ss[0], phi_ss[1], filename='twigl-ss-contour')