1 /* ---------------------------------------------------------------------
2  *
3  * Copyright (C) 2009 - 2020 by the deal.II authors
4  *
5  * This file is part of the deal.II library.
6  *
7  * The deal.II library is free software; you can use it, redistribute
8  * it, and/or modify it under the terms of the GNU Lesser General
9  * Public License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  * The full text of the license can be found in the file LICENSE.md at
12  * the top level directory of deal.II.
13  *
14  * ---------------------------------------------------------------------
15 
16  *
17  * Author: Yaqi Wang, Texas A&M University, 2009, 2010
18  */
19 
20 
21 // @sect3{Include files}
22 
23 // We start with a bunch of include files that have already been explained in
24 // previous tutorial programs. One new one is <code>timer.h</code>: This is
25 // the first example program that uses the Timer class. The Timer keeps track
26 // of both the elapsed wall clock time (that is, the amount of time that a
27 // clock mounted on the wall would measure) and CPU clock time (the amount of
28 // time that the current process uses on the CPUs). We will use a Timer below
29 // to measure how much CPU time each grid refinement cycle takes.
30 #include <deal.II/base/timer.h>
31 #include <deal.II/base/quadrature_lib.h>
32 #include <deal.II/base/function.h>
33 #include <deal.II/base/parameter_handler.h>
34 #include <deal.II/base/thread_management.h>
35 #include <deal.II/base/utilities.h>
36 
37 #include <deal.II/lac/vector.h>
38 #include <deal.II/lac/full_matrix.h>
39 #include <deal.II/lac/sparsity_pattern.h>
40 #include <deal.II/lac/dynamic_sparsity_pattern.h>
41 #include <deal.II/lac/sparse_matrix.h>
42 #include <deal.II/lac/solver_cg.h>
43 #include <deal.II/lac/precondition.h>
44 #include <deal.II/lac/affine_constraints.h>
45 
46 #include <deal.II/grid/tria.h>
47 #include <deal.II/grid/grid_refinement.h>
48 #include <deal.II/grid/grid_out.h>
49 #include <deal.II/grid/grid_generator.h>
50 #include <deal.II/grid/tria_accessor.h>
51 #include <deal.II/grid/tria_iterator.h>
52 
53 #include <deal.II/dofs/dof_handler.h>
54 #include <deal.II/dofs/dof_accessor.h>
55 #include <deal.II/dofs/dof_tools.h>
56 
57 #include <deal.II/fe/fe_q.h>
58 #include <deal.II/fe/fe_values.h>
59 
60 #include <deal.II/numerics/vector_tools.h>
61 #include <deal.II/numerics/matrix_tools.h>
62 #include <deal.II/numerics/data_out.h>
63 #include <deal.II/numerics/error_estimator.h>
64 
65 #include <fstream>
66 #include <iostream>
67 
68 
69 // We use the next include file to access block vectors which provide us a
70 // convenient way to manage solution and right hand side vectors of all energy
71 // groups:
72 #include <deal.II/lac/block_vector.h>
73 
74 // This include file is for transferring solutions from one mesh to another
75 // different mesh. We use it when we are initializing solutions after each
76 // mesh iteration:
77 #include <deal.II/numerics/solution_transfer.h>
78 
79 // When integrating functions defined on one mesh against shape functions
80 // defined on a different mesh, we need a function @p get_finest_common_cells
81 // (as discussed in the introduction) which is defined in the following header
82 // file:
83 #include <deal.II/grid/grid_tools.h>
84 
85 // We use a little utility class from boost to save the state of an output
86 // stream (see the <code>run</code> function below):
87 #include <boost/io/ios_state.hpp>
88 
89 // Here are two more C++ standard headers that we use to define list data
90 // types as well as to fine-tune the output we generate:
91 #include <list>
92 #include <iomanip>
93 
94 // The last step is as in all previous programs:
95 namespace Step28
96 {
97   using namespace dealii;
98 
99 
100   // @sect3{Material data}
101 
102   // First up, we need to define a class that provides material data
103   // (including diffusion coefficients, removal cross sections, scattering
104   // cross sections, fission cross sections and fission spectra) to the main
105   // class.
106   //
107   // The parameter to the constructor determines for how many energy groups we
108   // set up the relevant tables. At present, this program only includes data
109   // for 2 energy groups, but a more sophisticated program may be able to
110   // initialize the data structures for more groups as well, depending on how
111   // many energy groups are selected in the parameter file.
112   //
113   // For each of the different coefficient types, there is one function that
114   // returns the value of this coefficient for a particular energy group (or
115   // combination of energy groups, as for the distribution cross section
116   // $\chi_g\nu\Sigma_{f,g'}$ or scattering cross section $\Sigma_{s,g'\to
117   // g}$). In addition to the energy group or groups, these coefficients
118   // depend on the type of fuel or control rod, as explained in the
119   // introduction. The functions therefore take an additional parameter, @p
120   // material_id, that identifies the particular kind of rod. Within this
121   // program, we use <code>n_materials=8</code> different kinds of rods.
122   //
123   // Except for the scattering cross section, each of the coefficients
124   // therefore can be represented as an entry in a two-dimensional array of
125   // floating point values indexed by the energy group number as well as the
126   // material ID. The Table class template is the ideal way to store such
127   // data. Finally, the scattering coefficient depends on both two energy
128   // group indices and therefore needs to be stored in a three-dimensional
129   // array, for which we again use the Table class, where this time the first
130   // template argument (denoting the dimensionality of the array) of course
131   // needs to be three:
132   class MaterialData
133   {
134   public:
135     MaterialData(const unsigned int n_groups);
136 
137     double get_diffusion_coefficient(const unsigned int group,
138                                      const unsigned int material_id) const;
139     double get_removal_XS(const unsigned int group,
140                           const unsigned int material_id) const;
141     double get_fission_XS(const unsigned int group,
142                           const unsigned int material_id) const;
143     double get_fission_dist_XS(const unsigned int group_1,
144                                const unsigned int group_2,
145                                const unsigned int material_id) const;
146     double get_scattering_XS(const unsigned int group_1,
147                              const unsigned int group_2,
148                              const unsigned int material_id) const;
149     double get_fission_spectrum(const unsigned int group,
150                                 const unsigned int material_id) const;
151 
152   private:
153     const unsigned int n_groups;
154     const unsigned int n_materials;
155 
156     Table<2, double> diffusion;
157     Table<2, double> sigma_r;
158     Table<2, double> nu_sigma_f;
159     Table<3, double> sigma_s;
160     Table<2, double> chi;
161   };
162 
163   // The constructor of the class is used to initialize all the material data
164   // arrays. It takes the number of energy groups as an argument (an throws an
165   // error if that value is not equal to two, since at presently only data for
166   // two energy groups is implemented; however, using this, the function
167   // remains flexible and extendable into the future). In the member
168   // initialization part at the beginning, it also resizes the arrays to their
169   // correct sizes.
170   //
171   // At present, material data is stored for 8 different types of
172   // material. This, as well, may easily be extended in the future.
MaterialData(const unsigned int n_groups)173   MaterialData::MaterialData(const unsigned int n_groups)
174     : n_groups(n_groups)
175     , n_materials(8)
176     , diffusion(n_materials, n_groups)
177     , sigma_r(n_materials, n_groups)
178     , nu_sigma_f(n_materials, n_groups)
179     , sigma_s(n_materials, n_groups, n_groups)
180     , chi(n_materials, n_groups)
181   {
182     switch (this->n_groups)
183       {
184         case 2:
185           {
186             for (unsigned int m = 0; m < n_materials; ++m)
187               {
188                 diffusion[m][0] = 1.2;
189                 diffusion[m][1] = 0.4;
190                 chi[m][0]       = 1.0;
191                 chi[m][1]       = 0.0;
192                 sigma_r[m][0]   = 0.03;
193                 for (unsigned int group_1 = 0; group_1 < n_groups; ++group_1)
194                   for (unsigned int group_2 = 0; group_2 < n_groups; ++group_2)
195                     sigma_s[m][group_1][group_2] = 0.0;
196               }
197 
198 
199             diffusion[5][1] = 0.2;
200 
201             sigma_r[4][0] = 0.026;
202             sigma_r[5][0] = 0.051;
203             sigma_r[6][0] = 0.026;
204             sigma_r[7][0] = 0.050;
205 
206             sigma_r[0][1] = 0.100;
207             sigma_r[1][1] = 0.200;
208             sigma_r[2][1] = 0.250;
209             sigma_r[3][1] = 0.300;
210             sigma_r[4][1] = 0.020;
211             sigma_r[5][1] = 0.040;
212             sigma_r[6][1] = 0.020;
213             sigma_r[7][1] = 0.800;
214 
215             nu_sigma_f[0][0] = 0.0050;
216             nu_sigma_f[1][0] = 0.0075;
217             nu_sigma_f[2][0] = 0.0075;
218             nu_sigma_f[3][0] = 0.0075;
219             nu_sigma_f[4][0] = 0.000;
220             nu_sigma_f[5][0] = 0.000;
221             nu_sigma_f[6][0] = 1e-7;
222             nu_sigma_f[7][0] = 0.00;
223 
224             nu_sigma_f[0][1] = 0.125;
225             nu_sigma_f[1][1] = 0.300;
226             nu_sigma_f[2][1] = 0.375;
227             nu_sigma_f[3][1] = 0.450;
228             nu_sigma_f[4][1] = 0.000;
229             nu_sigma_f[5][1] = 0.000;
230             nu_sigma_f[6][1] = 3e-6;
231             nu_sigma_f[7][1] = 0.00;
232 
233             sigma_s[0][0][1] = 0.020;
234             sigma_s[1][0][1] = 0.015;
235             sigma_s[2][0][1] = 0.015;
236             sigma_s[3][0][1] = 0.015;
237             sigma_s[4][0][1] = 0.025;
238             sigma_s[5][0][1] = 0.050;
239             sigma_s[6][0][1] = 0.025;
240             sigma_s[7][0][1] = 0.010;
241 
242             break;
243           }
244 
245 
246         default:
247           Assert(false,
248                  ExcMessage(
249                    "Presently, only data for 2 groups is implemented"));
250       }
251   }
252 
253 
254   // Next are the functions that return the coefficient values for given
255   // materials and energy groups. All they do is to make sure that the given
256   // arguments are within the allowed ranges, and then look the respective
257   // value up in the corresponding tables:
258   double
get_diffusion_coefficient(const unsigned int group,const unsigned int material_id) const259   MaterialData::get_diffusion_coefficient(const unsigned int group,
260                                           const unsigned int material_id) const
261   {
262     Assert(group < n_groups, ExcIndexRange(group, 0, n_groups));
263     Assert(material_id < n_materials,
264            ExcIndexRange(material_id, 0, n_materials));
265 
266     return diffusion[material_id][group];
267   }
268 
269 
270 
get_removal_XS(const unsigned int group,const unsigned int material_id) const271   double MaterialData::get_removal_XS(const unsigned int group,
272                                       const unsigned int material_id) const
273   {
274     Assert(group < n_groups, ExcIndexRange(group, 0, n_groups));
275     Assert(material_id < n_materials,
276            ExcIndexRange(material_id, 0, n_materials));
277 
278     return sigma_r[material_id][group];
279   }
280 
281 
get_fission_XS(const unsigned int group,const unsigned int material_id) const282   double MaterialData::get_fission_XS(const unsigned int group,
283                                       const unsigned int material_id) const
284   {
285     Assert(group < n_groups, ExcIndexRange(group, 0, n_groups));
286     Assert(material_id < n_materials,
287            ExcIndexRange(material_id, 0, n_materials));
288 
289     return nu_sigma_f[material_id][group];
290   }
291 
292 
293 
get_scattering_XS(const unsigned int group_1,const unsigned int group_2,const unsigned int material_id) const294   double MaterialData::get_scattering_XS(const unsigned int group_1,
295                                          const unsigned int group_2,
296                                          const unsigned int material_id) const
297   {
298     Assert(group_1 < n_groups, ExcIndexRange(group_1, 0, n_groups));
299     Assert(group_2 < n_groups, ExcIndexRange(group_2, 0, n_groups));
300     Assert(material_id < n_materials,
301            ExcIndexRange(material_id, 0, n_materials));
302 
303     return sigma_s[material_id][group_1][group_2];
304   }
305 
306 
307 
308   double
get_fission_spectrum(const unsigned int group,const unsigned int material_id) const309   MaterialData::get_fission_spectrum(const unsigned int group,
310                                      const unsigned int material_id) const
311   {
312     Assert(group < n_groups, ExcIndexRange(group, 0, n_groups));
313     Assert(material_id < n_materials,
314            ExcIndexRange(material_id, 0, n_materials));
315 
316     return chi[material_id][group];
317   }
318 
319 
320   // The function computing the fission distribution cross section is slightly
321   // different, since it computes its value as the product of two other
322   // coefficients. We don't need to check arguments here, since this already
323   // happens when we call the two other functions involved, even though it
324   // would probably not hurt either:
get_fission_dist_XS(const unsigned int group_1,const unsigned int group_2,const unsigned int material_id) const325   double MaterialData::get_fission_dist_XS(const unsigned int group_1,
326                                            const unsigned int group_2,
327                                            const unsigned int material_id) const
328   {
329     return (get_fission_spectrum(group_1, material_id) *
330             get_fission_XS(group_2, material_id));
331   }
332 
333 
334 
335   // @sect3{The <code>EnergyGroup</code> class}
336 
337   // The first interesting class is the one that contains everything that is
338   // specific to a single energy group. To group things that belong together
339   // into individual objects, we declare a structure that holds the
340   // Triangulation and DoFHandler objects for the mesh used for a single
341   // energy group, and a number of other objects and member functions that we
342   // will discuss in the following sections.
343   //
344   // The main reason for this class is as follows: for both the forward
345   // problem (with a specified right hand side) as well as for the eigenvalue
346   // problem, one typically solves a sequence of problems for a single energy
347   // group each, rather than the fully coupled problem. This becomes
348   // understandable once one realizes that the system matrix for a single
349   // energy group is symmetric and positive definite (it is simply a diffusion
350   // operator), whereas the matrix for the fully coupled problem is generally
351   // nonsymmetric and not definite. It is also very large and quite full if
352   // more than a few energy groups are involved.
353   //
354   // Let us first look at the equation to solve in the case of an external
355   // right hand side (for the time independent case): @f{eqnarray*} -\nabla
356   // \cdot(D_g(x) \nabla \phi_g(x)) + \Sigma_{r,g}(x)\phi_g(x) =
357   // \chi_g\sum_{g'=1}^G\nu\Sigma_{f,g'}(x)\phi_{g'}(x) + \sum_{g'\ne
358   // g}\Sigma_{s,g'\to g}(x)\phi_{g'}(x) + s_{\mathrm{ext},g}(x) @f}
359   //
360   // We would typically solve this equation by moving all the terms on the
361   // right hand side with $g'=g$ to the left hand side, and solving for
362   // $\phi_g$. Of course, we don't know $\phi_{g'}$ yet, since the equations
363   // for those variables include right hand side terms involving
364   // $\phi_g$. What one typically does in such situations is to iterate:
365   // compute @f{eqnarray*} -\nabla \cdot(D_g(x) \nabla \phi^{(n)}_g(x)) &+&
366   // \Sigma_{r,g}(x)\phi^{(n)}_g(x) \\ &=&
367   // \chi_g\sum_{g'=1}^{g-1}\nu\Sigma_{f,g'}(x)\phi^{(n)}_{g'}(x) +
368   // \chi_g\sum_{g'=g}^G\nu\Sigma_{f,g'}(x)\phi^{(n-1)}_{g'}(x) + \sum_{g'\ne
369   // g, g'<g}\Sigma_{s,g'\to g}(x)\phi^{(n)}_{g'}(x) + \sum_{g'\ne g,
370   // g'>g}\Sigma_{s,g'\to g}(x)\phi^{(n-1)}_{g'}(x) + s_{\mathrm{ext},g}(x)
371   // @f}
372   //
373   // In other words, we solve the equation one by one, using values for
374   // $\phi_{g'}$ from the previous iteration $n-1$ if $g'\ge g$ and already
375   // computed values for $\phi_{g'}$ from the present iteration if $g'<g$.
376   //
377   // When computing the eigenvalue, we do a very similar iteration, except
378   // that we have no external right hand side and that the solution is scaled
379   // after each iteration as explained in the introduction.
380   //
381   // In either case, these two cases can be treated jointly if all we do is to
382   // equip the following class with these abilities: (i) form the left hand
383   // side matrix, (ii) form the in-group right hand side contribution,
384   // i.e. involving the extraneous source, and (iii) form that contribution to
385   // the right hand side that stems from group $g'$. This class does exactly
386   // these tasks (as well as some book-keeping, such as mesh refinement,
387   // setting up matrices and vectors, etc). On the other hand, the class
388   // itself has no idea how many energy groups there are, and in particular
389   // how they interact, i.e. the decision of how the outer iteration looks
390   // (and consequently whether we solve an eigenvalue or a direct problem) is
391   // left to the NeutronDiffusionProblem class further down below in this
392   // program.
393   //
394   // So let us go through the class and its interface:
395   template <int dim>
396   class EnergyGroup
397   {
398   public:
399     // @sect5{<code>EnergyGroup</code> public member functions}
400     //
401     // The class has a good number of public member functions, since its the
402     // way it operates is controlled from the outside, and therefore all
403     // functions that do something significant need to be called from another
404     // class. Let's start off with book-keeping: the class obviously needs to
405     // know which energy group it represents, which material data to use, and
406     // from what coarse grid to start. The constructor takes this information
407     // and initializes the relevant member variables with that (see below).
408     //
409     // Then we also need functions that set up the linear system,
410     // i.e. correctly size the matrix and its sparsity pattern, etc, given a
411     // finite element object to use. The <code>setup_linear_system</code>
412     // function does that. Finally, for this initial block, there are two
413     // functions that return the number of active cells and degrees of freedom
414     // used in this object -- using this, we can make the triangulation and
415     // DoF handler member variables private, and do not have to grant external
416     // use to it, enhancing encapsulation:
417     EnergyGroup(const unsigned int        group,
418                 const MaterialData &      material_data,
419                 const Triangulation<dim> &coarse_grid,
420                 const FiniteElement<dim> &fe);
421 
422     void setup_linear_system();
423 
424     unsigned int n_active_cells() const;
425     unsigned int n_dofs() const;
426 
427     // Then there are functions that assemble the linear system for each
428     // iteration and the present energy group. Note that the matrix is
429     // independent of the iteration number, so only has to be computed once
430     // for each refinement cycle. The situation is a bit more involved for the
431     // right hand side that has to be updated in each inverse power iteration,
432     // and that is further complicated by the fact that computing it may
433     // involve several different meshes as explained in the introduction. To
434     // make things more flexible with regard to solving the forward or the
435     // eigenvalue problem, we split the computation of the right hand side
436     // into a function that assembles the extraneous source and in-group
437     // contributions (which we will call with a zero function as source terms
438     // for the eigenvalue problem) and one that computes contributions to the
439     // right hand side from another energy group:
440     void assemble_system_matrix();
441     void assemble_ingroup_rhs(const Function<dim> &extraneous_source);
442     void assemble_cross_group_rhs(const EnergyGroup<dim> &g_prime);
443 
444     // Next we need a set of functions that actually compute the solution of a
445     // linear system, and do something with it (such as computing the fission
446     // source contribution mentioned in the introduction, writing graphical
447     // information to an output file, computing error indicators, or actually
448     // refining the grid based on these criteria and thresholds for refinement
449     // and coarsening). All these functions will later be called from the
450     // driver class <code>NeutronDiffusionProblem</code>, or any other class
451     // you may want to implement to solve a problem involving the neutron flux
452     // equations:
453     void solve();
454 
455     double get_fission_source() const;
456 
457     void output_results(const unsigned int cycle) const;
458 
459     void estimate_errors(Vector<float> &error_indicators) const;
460 
461     void refine_grid(const Vector<float> &error_indicators,
462                      const double         refine_threshold,
463                      const double         coarsen_threshold);
464 
465     // @sect5{<code>EnergyGroup</code> public data members}
466     //
467     // As is good practice in object oriented programming, we hide most data
468     // members by making them private. However, we have to grant the class
469     // that drives the process access to the solution vector as well as the
470     // solution of the previous iteration, since in the power iteration, the
471     // solution vector is scaled in every iteration by the present guess of
472     // the eigenvalue we are looking for:
473   public:
474     Vector<double> solution;
475     Vector<double> solution_old;
476 
477 
478     // @sect5{<code>EnergyGroup</code> private data members}
479     //
480     // The rest of the data members are private. Compared to all the previous
481     // tutorial programs, the only new data members are an integer storing
482     // which energy group this object represents, and a reference to the
483     // material data object that this object's constructor gets passed from
484     // the driver class. Likewise, the constructor gets a reference to the
485     // finite element object we are to use.
486     //
487     // Finally, we have to apply boundary values to the linear system in each
488     // iteration, i.e. quite frequently. Rather than interpolating them every
489     // time, we interpolate them once on each new mesh and then store them
490     // along with all the other data of this class:
491   private:
492     const unsigned int  group;
493     const MaterialData &material_data;
494 
495     Triangulation<dim>        triangulation;
496     const FiniteElement<dim> &fe;
497     DoFHandler<dim>           dof_handler;
498 
499     SparsityPattern      sparsity_pattern;
500     SparseMatrix<double> system_matrix;
501 
502     Vector<double> system_rhs;
503 
504     std::map<types::global_dof_index, double> boundary_values;
505     AffineConstraints<double>                 hanging_node_constraints;
506 
507 
508     // @sect5{<code>EnergyGroup</code> private member functions}
509     //
510     // There is one private member function in this class. It recursively
511     // walks over cells of two meshes to compute the cross-group right hand
512     // side terms. The algorithm for this is explained in the introduction to
513     // this program. The arguments to this function are a reference to an
514     // object representing the energy group against which we want to integrate
515     // a right hand side term, an iterator to a cell of the mesh used for the
516     // present energy group, an iterator to a corresponding cell on the other
517     // mesh, and the matrix that interpolates the degrees of freedom from the
518     // coarser of the two cells to the finer one:
519   private:
520     void assemble_cross_group_rhs_recursive(
521       const EnergyGroup<dim> &                       g_prime,
522       const typename DoFHandler<dim>::cell_iterator &cell_g,
523       const typename DoFHandler<dim>::cell_iterator &cell_g_prime,
524       const FullMatrix<double> &                     prolongation_matrix);
525   };
526 
527 
528   // @sect4{Implementation of the <code>EnergyGroup</code> class}
529 
530   // The first few functions of this class are mostly self-explanatory. The
531   // constructor only sets a few data members and creates a copy of the given
532   // triangulation as the base for the triangulation used for this energy
533   // group. The next two functions simply return data from private data
534   // members, thereby enabling us to make these data members private.
535   template <int dim>
EnergyGroup(const unsigned int group,const MaterialData & material_data,const Triangulation<dim> & coarse_grid,const FiniteElement<dim> & fe)536   EnergyGroup<dim>::EnergyGroup(const unsigned int        group,
537                                 const MaterialData &      material_data,
538                                 const Triangulation<dim> &coarse_grid,
539                                 const FiniteElement<dim> &fe)
540     : group(group)
541     , material_data(material_data)
542     , fe(fe)
543     , dof_handler(triangulation)
544   {
545     triangulation.copy_triangulation(coarse_grid);
546     dof_handler.distribute_dofs(fe);
547   }
548 
549 
550 
551   template <int dim>
n_active_cells() const552   unsigned int EnergyGroup<dim>::n_active_cells() const
553   {
554     return triangulation.n_active_cells();
555   }
556 
557 
558 
559   template <int dim>
n_dofs() const560   unsigned int EnergyGroup<dim>::n_dofs() const
561   {
562     return dof_handler.n_dofs();
563   }
564 
565 
566 
567   // @sect5{<code>EnergyGroup::setup_linear_system</code>}
568   //
569   // The first "real" function is the one that sets up the mesh, matrices,
570   // etc, on the new mesh or after mesh refinement. We use this function to
571   // initialize sparse system matrices, and the right hand side vector. If the
572   // solution vector has never been set before (as indicated by a zero size),
573   // we also initialize it and set it to a default value. We don't do that if
574   // it already has a non-zero size (i.e. this function is called after mesh
575   // refinement) since in that case we want to preserve the solution across
576   // mesh refinement (something we do in the
577   // <code>EnergyGroup::refine_grid</code> function).
578   template <int dim>
setup_linear_system()579   void EnergyGroup<dim>::setup_linear_system()
580   {
581     const unsigned int n_dofs = dof_handler.n_dofs();
582 
583     hanging_node_constraints.clear();
584     DoFTools::make_hanging_node_constraints(dof_handler,
585                                             hanging_node_constraints);
586     hanging_node_constraints.close();
587 
588     system_matrix.clear();
589 
590     DynamicSparsityPattern dsp(n_dofs, n_dofs);
591     DoFTools::make_sparsity_pattern(dof_handler, dsp);
592     hanging_node_constraints.condense(dsp);
593     sparsity_pattern.copy_from(dsp);
594 
595     system_matrix.reinit(sparsity_pattern);
596 
597     system_rhs.reinit(n_dofs);
598 
599     if (solution.size() == 0)
600       {
601         solution.reinit(n_dofs);
602         solution_old.reinit(n_dofs);
603         solution_old = 1.0;
604         solution     = solution_old;
605       }
606 
607 
608     // At the end of this function, we update the list of boundary nodes and
609     // their values, by first clearing this list and the re-interpolating
610     // boundary values (remember that this function is called after first
611     // setting up the mesh, and each time after mesh refinement).
612     //
613     // To understand the code, it is necessary to realize that we create the
614     // mesh using the <code>GridGenerator::subdivided_hyper_rectangle</code>
615     // function (in <code>NeutronDiffusionProblem::initialize_problem</code>)
616     // where we set the last parameter to <code>true</code>. This means that
617     // boundaries of the domain are "colored", i.e. the four (or six, in 3d)
618     // sides of the domain are assigned different boundary indicators. As it
619     // turns out, the bottom boundary gets indicator zero, the top one
620     // boundary indicator one, and left and right boundaries get indicators
621     // two and three, respectively.
622     //
623     // In this program, we simulate only one, namely the top right, quarter of
624     // a reactor. That is, we want to interpolate boundary conditions only on
625     // the top and right boundaries, while do nothing on the bottom and left
626     // boundaries (i.e. impose natural, no-flux Neumann boundary
627     // conditions). This is most easily generalized to arbitrary dimension by
628     // saying that we want to interpolate on those boundaries with indicators
629     // 1, 3, ..., which we do in the following loop (note that calls to
630     // <code>VectorTools::interpolate_boundary_values</code> are additive,
631     // i.e. they do not first clear the boundary value map):
632     boundary_values.clear();
633 
634     for (unsigned int i = 0; i < dim; ++i)
635       VectorTools::interpolate_boundary_values(dof_handler,
636                                                2 * i + 1,
637                                                Functions::ZeroFunction<dim>(),
638                                                boundary_values);
639   }
640 
641 
642 
643   // @sect5{<code>EnergyGroup::assemble_system_matrix</code>}
644   //
645   // Next we need functions assembling the system matrix and right hand
646   // sides. Assembling the matrix is straightforward given the equations
647   // outlined in the introduction as well as what we've seen in previous
648   // example programs. Note the use of <code>cell->material_id()</code> to get
649   // at the kind of material from which a cell is made up of. Note also how we
650   // set the order of the quadrature formula so that it is always appropriate
651   // for the finite element in use.
652   //
653   // Finally, note that since we only assemble the system matrix here, we
654   // can't yet eliminate boundary values (we need the right hand side vector
655   // for this). We defer this to the <code>EnergyGroup::solve</code> function,
656   // at which point all the information is available.
657   template <int dim>
assemble_system_matrix()658   void EnergyGroup<dim>::assemble_system_matrix()
659   {
660     const QGauss<dim> quadrature_formula(fe.degree + 1);
661 
662     FEValues<dim> fe_values(fe,
663                             quadrature_formula,
664                             update_values | update_gradients |
665                               update_JxW_values);
666 
667     const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
668     const unsigned int n_q_points    = quadrature_formula.size();
669 
670     FullMatrix<double> cell_matrix(dofs_per_cell, dofs_per_cell);
671     Vector<double>     cell_rhs(dofs_per_cell);
672 
673     std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
674 
675     for (const auto &cell : dof_handler.active_cell_iterators())
676       {
677         cell_matrix = 0;
678 
679         fe_values.reinit(cell);
680 
681         const double diffusion_coefficient =
682           material_data.get_diffusion_coefficient(group, cell->material_id());
683         const double removal_XS =
684           material_data.get_removal_XS(group, cell->material_id());
685 
686         for (unsigned int q_point = 0; q_point < n_q_points; ++q_point)
687           for (unsigned int i = 0; i < dofs_per_cell; ++i)
688             for (unsigned int j = 0; j < dofs_per_cell; ++j)
689               cell_matrix(i, j) +=
690                 ((diffusion_coefficient * fe_values.shape_grad(i, q_point) *
691                     fe_values.shape_grad(j, q_point) +
692                   removal_XS * fe_values.shape_value(i, q_point) *
693                     fe_values.shape_value(j, q_point)) *
694                  fe_values.JxW(q_point));
695 
696         cell->get_dof_indices(local_dof_indices);
697 
698         for (unsigned int i = 0; i < dofs_per_cell; ++i)
699           for (unsigned int j = 0; j < dofs_per_cell; ++j)
700             system_matrix.add(local_dof_indices[i],
701                               local_dof_indices[j],
702                               cell_matrix(i, j));
703       }
704 
705     hanging_node_constraints.condense(system_matrix);
706   }
707 
708 
709 
710   // @sect5{<code>EnergyGroup::assemble_ingroup_rhs</code>}
711   //
712   // As explained in the documentation of the <code>EnergyGroup</code> class,
713   // we split assembling the right hand side into two parts: the ingroup and
714   // the cross-group couplings. First, we need a function to assemble the
715   // right hand side of one specific group here, i.e. including an extraneous
716   // source (that we will set to zero for the eigenvalue problem) as well as
717   // the ingroup fission contributions.  (In-group scattering has already been
718   // accounted for with the definition of removal cross section.) The
719   // function's workings are pretty standard as far as assembling right hand
720   // sides go, and therefore does not require more comments except that we
721   // mention that the right hand side vector is set to zero at the beginning
722   // of the function -- something we are not going to do for the cross-group
723   // terms that simply add to the right hand side vector.
724   template <int dim>
725   void
assemble_ingroup_rhs(const Function<dim> & extraneous_source)726   EnergyGroup<dim>::assemble_ingroup_rhs(const Function<dim> &extraneous_source)
727   {
728     system_rhs.reinit(dof_handler.n_dofs());
729 
730     const QGauss<dim> quadrature_formula(fe.degree + 1);
731 
732     const unsigned int dofs_per_cell = fe.n_dofs_per_cell();
733     const unsigned int n_q_points    = quadrature_formula.size();
734 
735     FEValues<dim> fe_values(fe,
736                             quadrature_formula,
737                             update_values | update_quadrature_points |
738                               update_JxW_values);
739 
740     Vector<double>      cell_rhs(dofs_per_cell);
741     std::vector<double> extraneous_source_values(n_q_points);
742     std::vector<double> solution_old_values(n_q_points);
743 
744     std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
745 
746     for (const auto &cell : dof_handler.active_cell_iterators())
747       {
748         cell_rhs = 0;
749 
750         fe_values.reinit(cell);
751 
752         const double fission_dist_XS =
753           material_data.get_fission_dist_XS(group, group, cell->material_id());
754 
755         extraneous_source.value_list(fe_values.get_quadrature_points(),
756                                      extraneous_source_values);
757 
758         fe_values.get_function_values(solution_old, solution_old_values);
759 
760         cell->get_dof_indices(local_dof_indices);
761 
762         for (unsigned int q_point = 0; q_point < n_q_points; ++q_point)
763           for (unsigned int i = 0; i < dofs_per_cell; ++i)
764             cell_rhs(i) +=
765               ((extraneous_source_values[q_point] +
766                 fission_dist_XS * solution_old_values[q_point]) *
767                fe_values.shape_value(i, q_point) * fe_values.JxW(q_point));
768 
769         for (unsigned int i = 0; i < dofs_per_cell; ++i)
770           system_rhs(local_dof_indices[i]) += cell_rhs(i);
771       }
772   }
773 
774 
775 
776   // @sect5{<code>EnergyGroup::assemble_cross_group_rhs</code>}
777   //
778   // The more interesting function for assembling the right hand side vector
779   // for the equation of a single energy group is the one that couples energy
780   // group $g$ and $g'$. As explained in the introduction, we first have to
781   // find the set of cells common to the meshes of the two energy
782   // groups. First we call <code>get_finest_common_cells</code> to obtain this
783   // list of pairs of common cells from both meshes. Both cells in a pair may
784   // not be active but at least one of them is. We then hand each of these
785   // cell pairs off to a function that computes the right hand side terms
786   // recursively.
787   //
788   // Note that ingroup coupling is handled already before, so we exit the
789   // function early if $g=g'$.
790   template <int dim>
791   void
assemble_cross_group_rhs(const EnergyGroup<dim> & g_prime)792   EnergyGroup<dim>::assemble_cross_group_rhs(const EnergyGroup<dim> &g_prime)
793   {
794     if (group == g_prime.group)
795       return;
796 
797     const std::list<std::pair<typename DoFHandler<dim>::cell_iterator,
798                               typename DoFHandler<dim>::cell_iterator>>
799       cell_list =
800         GridTools::get_finest_common_cells(dof_handler, g_prime.dof_handler);
801 
802     for (const auto &cell_pair : cell_list)
803       {
804         FullMatrix<double> unit_matrix(fe.n_dofs_per_cell());
805         for (unsigned int i = 0; i < unit_matrix.m(); ++i)
806           unit_matrix(i, i) = 1;
807         assemble_cross_group_rhs_recursive(g_prime,
808                                            cell_pair.first,
809                                            cell_pair.second,
810                                            unit_matrix);
811       }
812   }
813 
814 
815 
816   // @sect5{<code>EnergyGroup::assemble_cross_group_rhs_recursive</code>}
817   //
818   // This is finally the function that handles assembling right hand side
819   // terms on potentially different meshes recursively, using the algorithm
820   // described in the introduction. The function takes a reference to the
821   // object representing energy group $g'$, as well as iterators to
822   // corresponding cells in the meshes for energy groups $g$ and $g'$. At
823   // first, i.e. when this function is called from the one above, these two
824   // cells will be matching cells on two meshes; however, one of the two may
825   // be further refined, and we will call the function recursively with one of
826   // the two iterators replaced by one of the children of the original cell.
827   //
828   // The last argument is the matrix product matrix $B_{c^{(k)}}^T \cdots
829   // B_{c'}^T B_c^T$ from the introduction that interpolates from the coarser
830   // of the two cells to the finer one. If the two cells match, then this is
831   // the identity matrix -- exactly what we pass to this function initially.
832   //
833   // The function has to consider two cases: that both of the two cells are
834   // not further refined, i.e. have no children, in which case we can finally
835   // assemble the right hand side contributions of this pair of cells; and
836   // that one of the two cells is further refined, in which case we have to
837   // keep recursing by looping over the children of the one cell that is not
838   // active. These two cases will be discussed below:
839   template <int dim>
assemble_cross_group_rhs_recursive(const EnergyGroup<dim> & g_prime,const typename DoFHandler<dim>::cell_iterator & cell_g,const typename DoFHandler<dim>::cell_iterator & cell_g_prime,const FullMatrix<double> & prolongation_matrix)840   void EnergyGroup<dim>::assemble_cross_group_rhs_recursive(
841     const EnergyGroup<dim> &                       g_prime,
842     const typename DoFHandler<dim>::cell_iterator &cell_g,
843     const typename DoFHandler<dim>::cell_iterator &cell_g_prime,
844     const FullMatrix<double> &                     prolongation_matrix)
845   {
846     // The first case is that both cells are no further refined. In that case,
847     // we can assemble the relevant terms (see the introduction). This
848     // involves assembling the mass matrix on the finer of the two cells (in
849     // fact there are two mass matrices with different coefficients, one for
850     // the fission distribution cross section $\chi_g\nu\Sigma_{f,g'}$ and one
851     // for the scattering cross section $\Sigma_{s,g'\to g}$). This is
852     // straight forward, but note how we determine which of the two cells is
853     // the finer one by looking at the refinement level of the two cells:
854     if (!cell_g->has_children() && !cell_g_prime->has_children())
855       {
856         const QGauss<dim>  quadrature_formula(fe.degree + 1);
857         const unsigned int n_q_points = quadrature_formula.size();
858 
859         FEValues<dim> fe_values(fe,
860                                 quadrature_formula,
861                                 update_values | update_JxW_values);
862 
863         if (cell_g->level() > cell_g_prime->level())
864           fe_values.reinit(cell_g);
865         else
866           fe_values.reinit(cell_g_prime);
867 
868         const double fission_dist_XS =
869           material_data.get_fission_dist_XS(group,
870                                             g_prime.group,
871                                             cell_g_prime->material_id());
872 
873         const double scattering_XS =
874           material_data.get_scattering_XS(g_prime.group,
875                                           group,
876                                           cell_g_prime->material_id());
877 
878         FullMatrix<double> local_mass_matrix_f(fe.n_dofs_per_cell(),
879                                                fe.n_dofs_per_cell());
880         FullMatrix<double> local_mass_matrix_g(fe.n_dofs_per_cell(),
881                                                fe.n_dofs_per_cell());
882 
883         for (unsigned int q_point = 0; q_point < n_q_points; ++q_point)
884           for (unsigned int i = 0; i < fe.n_dofs_per_cell(); ++i)
885             for (unsigned int j = 0; j < fe.n_dofs_per_cell(); ++j)
886               {
887                 local_mass_matrix_f(i, j) +=
888                   (fission_dist_XS * fe_values.shape_value(i, q_point) *
889                    fe_values.shape_value(j, q_point) * fe_values.JxW(q_point));
890                 local_mass_matrix_g(i, j) +=
891                   (scattering_XS * fe_values.shape_value(i, q_point) *
892                    fe_values.shape_value(j, q_point) * fe_values.JxW(q_point));
893               }
894 
895         // Now we have all the interpolation (prolongation) matrices as well
896         // as local mass matrices, so we only have to form the product @f[
897         // F_i|_{K_{cc'\cdots c^{(k)}}} = [B_c B_{c'} \cdots B_{c^{(k)}}
898         // M_{K_{cc'\cdots c^{(k)}}}]^{ij} \phi_{g'}^j, @f] or @f[
899         // F_i|_{K_{cc'\cdots c^{(k)}}} = [(B_c B_{c'} \cdots B_{c^{(k)}}
900         // M_{K_{cc'\cdots c^{(k)}}})^T]^{ij} \phi_{g'}^j, @f] depending on
901         // which of the two cells is the finer. We do this using either the
902         // matrix-vector product provided by the <code>vmult</code> function,
903         // or the product with the transpose matrix using <code>Tvmult</code>.
904         // After doing so, we transfer the result into the global right hand
905         // side vector of energy group $g$.
906         Vector<double> g_prime_new_values(fe.n_dofs_per_cell());
907         Vector<double> g_prime_old_values(fe.n_dofs_per_cell());
908         cell_g_prime->get_dof_values(g_prime.solution_old, g_prime_old_values);
909         cell_g_prime->get_dof_values(g_prime.solution, g_prime_new_values);
910 
911         Vector<double> cell_rhs(fe.n_dofs_per_cell());
912         Vector<double> tmp(fe.n_dofs_per_cell());
913 
914         if (cell_g->level() > cell_g_prime->level())
915           {
916             prolongation_matrix.vmult(tmp, g_prime_old_values);
917             local_mass_matrix_f.vmult(cell_rhs, tmp);
918 
919             prolongation_matrix.vmult(tmp, g_prime_new_values);
920             local_mass_matrix_g.vmult_add(cell_rhs, tmp);
921           }
922         else
923           {
924             local_mass_matrix_f.vmult(tmp, g_prime_old_values);
925             prolongation_matrix.Tvmult(cell_rhs, tmp);
926 
927             local_mass_matrix_g.vmult(tmp, g_prime_new_values);
928             prolongation_matrix.Tvmult_add(cell_rhs, tmp);
929           }
930 
931         std::vector<types::global_dof_index> local_dof_indices(
932           fe.n_dofs_per_cell());
933         cell_g->get_dof_indices(local_dof_indices);
934 
935         for (unsigned int i = 0; i < fe.n_dofs_per_cell(); ++i)
936           system_rhs(local_dof_indices[i]) += cell_rhs(i);
937       }
938 
939     // The alternative is that one of the two cells is further refined. In
940     // that case, we have to loop over all the children, multiply the existing
941     // interpolation (prolongation) product of matrices from the left with the
942     // interpolation from the present cell to its child (using the
943     // matrix-matrix multiplication function <code>mmult</code>), and then
944     // hand the result off to this very same function again, but with the cell
945     // that has children replaced by one of its children:
946     else
947       for (unsigned int child = 0;
948            child < GeometryInfo<dim>::max_children_per_cell;
949            ++child)
950         {
951           FullMatrix<double> new_matrix(fe.n_dofs_per_cell(),
952                                         fe.n_dofs_per_cell());
953           fe.get_prolongation_matrix(child).mmult(new_matrix,
954                                                   prolongation_matrix);
955 
956           if (cell_g->has_children())
957             assemble_cross_group_rhs_recursive(g_prime,
958                                                cell_g->child(child),
959                                                cell_g_prime,
960                                                new_matrix);
961           else
962             assemble_cross_group_rhs_recursive(g_prime,
963                                                cell_g,
964                                                cell_g_prime->child(child),
965                                                new_matrix);
966         }
967   }
968 
969 
970   // @sect5{<code>EnergyGroup::get_fission_source</code>}
971   //
972   // In the (inverse) power iteration, we use the integrated fission source to
973   // update the $k$-eigenvalue. Given its definition, the following function
974   // is essentially self-explanatory:
975   template <int dim>
get_fission_source() const976   double EnergyGroup<dim>::get_fission_source() const
977   {
978     const QGauss<dim>  quadrature_formula(fe.degree + 1);
979     const unsigned int n_q_points = quadrature_formula.size();
980 
981     FEValues<dim> fe_values(fe,
982                             quadrature_formula,
983                             update_values | update_JxW_values);
984 
985     std::vector<double> solution_values(n_q_points);
986 
987     double fission_source = 0;
988 
989     for (const auto &cell : dof_handler.active_cell_iterators())
990       {
991         fe_values.reinit(cell);
992 
993         const double fission_XS =
994           material_data.get_fission_XS(group, cell->material_id());
995 
996         fe_values.get_function_values(solution, solution_values);
997 
998         for (unsigned int q_point = 0; q_point < n_q_points; ++q_point)
999           fission_source +=
1000             (fission_XS * solution_values[q_point] * fe_values.JxW(q_point));
1001       }
1002 
1003     return fission_source;
1004   }
1005 
1006 
1007   // @sect5{<code>EnergyGroup::solve</code>}
1008   //
1009   // Next a function that solves the linear system assembled before. Things
1010   // are pretty much standard, except that we delayed applying boundary values
1011   // until we get here, since in all the previous functions we were still
1012   // adding up contributions the right hand side vector.
1013   template <int dim>
solve()1014   void EnergyGroup<dim>::solve()
1015   {
1016     hanging_node_constraints.condense(system_rhs);
1017     MatrixTools::apply_boundary_values(boundary_values,
1018                                        system_matrix,
1019                                        solution,
1020                                        system_rhs);
1021 
1022     SolverControl            solver_control(system_matrix.m(),
1023                                  1e-12 * system_rhs.l2_norm());
1024     SolverCG<Vector<double>> cg(solver_control);
1025 
1026     PreconditionSSOR<SparseMatrix<double>> preconditioner;
1027     preconditioner.initialize(system_matrix, 1.2);
1028 
1029     cg.solve(system_matrix, solution, system_rhs, preconditioner);
1030 
1031     hanging_node_constraints.distribute(solution);
1032   }
1033 
1034 
1035 
1036   // @sect5{<code>EnergyGroup::estimate_errors</code>}
1037   //
1038   // Mesh refinement is split into two functions. The first estimates the
1039   // error for each cell, normalizes it by the magnitude of the solution, and
1040   // returns it in the vector given as an argument. The calling function
1041   // collects all error indicators from all energy groups, and computes
1042   // thresholds for refining and coarsening cells.
1043   template <int dim>
estimate_errors(Vector<float> & error_indicators) const1044   void EnergyGroup<dim>::estimate_errors(Vector<float> &error_indicators) const
1045   {
1046     KellyErrorEstimator<dim>::estimate(
1047       dof_handler,
1048       QGauss<dim - 1>(fe.degree + 1),
1049       std::map<types::boundary_id, const Function<dim> *>(),
1050       solution,
1051       error_indicators);
1052     error_indicators /= solution.linfty_norm();
1053   }
1054 
1055 
1056 
1057   // @sect5{<code>EnergyGroup::refine_grid</code>}
1058   //
1059   // The second part is to refine the grid given the error indicators compute
1060   // in the previous function and error thresholds above which cells shall be
1061   // refined or below which cells shall be coarsened. Note that we do not use
1062   // any of the functions in <code>GridRefinement</code> here, but rather set
1063   // refinement flags ourselves.
1064   //
1065   // After setting these flags, we use the SolutionTransfer class to move the
1066   // solution vector from the old to the new mesh. The procedure used here is
1067   // described in detail in the documentation of that class:
1068   template <int dim>
refine_grid(const Vector<float> & error_indicators,const double refine_threshold,const double coarsen_threshold)1069   void EnergyGroup<dim>::refine_grid(const Vector<float> &error_indicators,
1070                                      const double         refine_threshold,
1071                                      const double         coarsen_threshold)
1072   {
1073     for (const auto &cell : triangulation.active_cell_iterators())
1074       if (error_indicators(cell->active_cell_index()) > refine_threshold)
1075         cell->set_refine_flag();
1076       else if (error_indicators(cell->active_cell_index()) < coarsen_threshold)
1077         cell->set_coarsen_flag();
1078 
1079     SolutionTransfer<dim> soltrans(dof_handler);
1080 
1081     triangulation.prepare_coarsening_and_refinement();
1082     soltrans.prepare_for_coarsening_and_refinement(solution);
1083 
1084     triangulation.execute_coarsening_and_refinement();
1085     dof_handler.distribute_dofs(fe);
1086     setup_linear_system();
1087 
1088     solution.reinit(dof_handler.n_dofs());
1089     soltrans.interpolate(solution_old, solution);
1090 
1091     // enforce constraints to make the interpolated solution conforming on
1092     // the new mesh:
1093     hanging_node_constraints.distribute(solution);
1094 
1095     solution_old.reinit(dof_handler.n_dofs());
1096     solution_old = solution;
1097   }
1098 
1099 
1100   // @sect5{<code>EnergyGroup::output_results</code>}
1101   //
1102   // The last function of this class outputs meshes and solutions after each
1103   // mesh iteration. This has been shown many times before. The only thing
1104   // worth pointing out is the use of the
1105   // <code>Utilities::int_to_string</code> function to convert an integer into
1106   // its string representation. The second argument of that function denotes
1107   // how many digits we shall use -- if this value was larger than one, then
1108   // the number would be padded by leading zeros.
1109   template <int dim>
output_results(const unsigned int cycle) const1110   void EnergyGroup<dim>::output_results(const unsigned int cycle) const
1111   {
1112     const std::string filename = std::string("solution-") +
1113                                  Utilities::int_to_string(group, 2) + "." +
1114                                  Utilities::int_to_string(cycle, 2) + ".vtu";
1115 
1116     DataOut<dim> data_out;
1117 
1118     data_out.attach_dof_handler(dof_handler);
1119     data_out.add_data_vector(solution, "solution");
1120     data_out.build_patches();
1121 
1122     std::ofstream output(filename);
1123     data_out.write_vtu(output);
1124   }
1125 
1126 
1127 
1128   // @sect3{The <code>NeutronDiffusionProblem</code> class template}
1129 
1130   // This is the main class of the program, not because it implements all the
1131   // functionality (in fact, most of it is implemented in the
1132   // <code>EnergyGroup</code> class) but because it contains the driving
1133   // algorithm that determines what to compute and when. It is mostly as shown
1134   // in many of the other tutorial programs in that it has a public
1135   // <code>run</code> function and private functions doing all the rest. In
1136   // several places, we have to do something for all energy groups, in which
1137   // case we will start tasks for each group to let these things run in
1138   // parallel if deal.II was configured for multithreading.  For strategies of
1139   // parallelization, take a look at the @ref threads module.
1140   //
1141   // The biggest difference to previous example programs is that we also
1142   // declare a nested class that has member variables for all the run-time
1143   // parameters that can be passed to the program in an input file. Right now,
1144   // these are the number of energy groups, the number of refinement cycles,
1145   // the polynomial degree of the finite element to be used, and the tolerance
1146   // used to determine when convergence of the inverse power iteration has
1147   // occurred. In addition, we have a constructor of this class that sets all
1148   // these values to their default values, a function
1149   // <code>declare_parameters</code> that describes to the ParameterHandler
1150   // class what parameters are accepted in the input file, and a function
1151   // <code>get_parameters</code> that can extract the values of these
1152   // parameters from a ParameterHandler object. See also step-29 for another
1153   // example of using ParameterHandler.
1154   template <int dim>
1155   class NeutronDiffusionProblem
1156   {
1157   public:
1158     class Parameters
1159     {
1160     public:
1161       Parameters();
1162 
1163       static void declare_parameters(ParameterHandler &prm);
1164       void        get_parameters(ParameterHandler &prm);
1165 
1166       unsigned int n_groups;
1167       unsigned int n_refinement_cycles;
1168 
1169       unsigned int fe_degree;
1170 
1171       double convergence_tolerance;
1172     };
1173 
1174     NeutronDiffusionProblem(const Parameters &parameters);
1175 
1176     void run();
1177 
1178   private:
1179     // @sect5{<code>NeutronDiffusionProblem</code> private member functions}
1180 
1181     // There are not that many member functions in this class since most of
1182     // the functionality has been moved into the <code>EnergyGroup</code>
1183     // class and is simply called from the <code>run()</code> member function
1184     // of this class. The ones that remain have self-explanatory names:
1185     void initialize_problem();
1186 
1187     void refine_grid();
1188 
1189     double get_total_fission_source() const;
1190 
1191 
1192     // @sect5{<code>NeutronDiffusionProblem</code> private member variables}
1193 
1194     // Next, we have a few member variables. In particular, these are (i) a
1195     // reference to the parameter object (owned by the main function of this
1196     // program, and passed to the constructor of this class), (ii) an object
1197     // describing the material parameters for the number of energy groups
1198     // requested in the input file, and (iii) the finite element to be used by
1199     // all energy groups:
1200     const Parameters & parameters;
1201     const MaterialData material_data;
1202     FE_Q<dim>          fe;
1203 
1204     // Furthermore, we have (iv) the value of the computed eigenvalue at the
1205     // present iteration. This is, in fact, the only part of the solution that
1206     // is shared between all energy groups -- all other parts of the solution,
1207     // such as neutron fluxes are particular to one or the other energy group,
1208     // and are therefore stored in objects that describe a single energy
1209     // group:
1210     double k_eff;
1211 
1212     // The last computational object (v) is an array of pointers to the energy
1213     // group objects. The length of this array is, of course, equal to the
1214     // number of energy groups specified in the parameter file.
1215     std::vector<std::unique_ptr<EnergyGroup<dim>>> energy_groups;
1216 
1217     // Finally (vi) we have a file stream to which we will save summarized
1218     // output.
1219     std::ofstream convergence_table_stream;
1220   };
1221 
1222 
1223   // @sect4{Implementation of the <code>Parameters</code> class}
1224 
1225   // Before going on to the implementation of the outer class, we have to
1226   // implement the functions of the parameters structure. This is pretty
1227   // straightforward and, in fact, looks pretty much the same for all such
1228   // parameters classes using the ParameterHandler capabilities. We will
1229   // therefore not comment further on this:
1230   template <int dim>
Parameters()1231   NeutronDiffusionProblem<dim>::Parameters::Parameters()
1232     : n_groups(2)
1233     , n_refinement_cycles(5)
1234     , fe_degree(2)
1235     , convergence_tolerance(1e-12)
1236   {}
1237 
1238 
1239 
1240   template <int dim>
declare_parameters(ParameterHandler & prm)1241   void NeutronDiffusionProblem<dim>::Parameters::declare_parameters(
1242     ParameterHandler &prm)
1243   {
1244     prm.declare_entry("Number of energy groups",
1245                       "2",
1246                       Patterns::Integer(),
1247                       "The number of energy different groups considered");
1248     prm.declare_entry("Refinement cycles",
1249                       "5",
1250                       Patterns::Integer(),
1251                       "Number of refinement cycles to be performed");
1252     prm.declare_entry("Finite element degree",
1253                       "2",
1254                       Patterns::Integer(),
1255                       "Polynomial degree of the finite element to be used");
1256     prm.declare_entry(
1257       "Power iteration tolerance",
1258       "1e-12",
1259       Patterns::Double(),
1260       "Inner power iterations are stopped when the change in k_eff falls "
1261       "below this tolerance");
1262   }
1263 
1264 
1265 
1266   template <int dim>
get_parameters(ParameterHandler & prm)1267   void NeutronDiffusionProblem<dim>::Parameters::get_parameters(
1268     ParameterHandler &prm)
1269   {
1270     n_groups              = prm.get_integer("Number of energy groups");
1271     n_refinement_cycles   = prm.get_integer("Refinement cycles");
1272     fe_degree             = prm.get_integer("Finite element degree");
1273     convergence_tolerance = prm.get_double("Power iteration tolerance");
1274   }
1275 
1276 
1277 
1278   // @sect4{Implementation of the <code>NeutronDiffusionProblem</code> class}
1279 
1280   // Now for the <code>NeutronDiffusionProblem</code> class. The constructor
1281   // and destructor have nothing of much interest:
1282   template <int dim>
NeutronDiffusionProblem(const Parameters & parameters)1283   NeutronDiffusionProblem<dim>::NeutronDiffusionProblem(
1284     const Parameters &parameters)
1285     : parameters(parameters)
1286     , material_data(parameters.n_groups)
1287     , fe(parameters.fe_degree)
1288     , k_eff(std::numeric_limits<double>::quiet_NaN())
1289   {}
1290 
1291 
1292 
1293   // @sect5{<code>NeutronDiffusionProblem::initialize_problem</code>}
1294   //
1295   // The first function of interest is the one that sets up the geometry of
1296   // the reactor core. This is described in more detail in the introduction.
1297   //
1298   // The first part of the function defines geometry data, and then creates a
1299   // coarse mesh that has as many cells as there are fuel rods (or pin cells,
1300   // for that matter) in that part of the reactor core that we simulate. As
1301   // mentioned when interpolating boundary values above, the last parameter to
1302   // the <code>GridGenerator::subdivided_hyper_rectangle</code> function
1303   // specifies that sides of the domain shall have unique boundary indicators
1304   // that will later allow us to determine in a simple way which of the
1305   // boundaries have Neumann and which have Dirichlet conditions attached to
1306   // them.
1307   template <int dim>
initialize_problem()1308   void NeutronDiffusionProblem<dim>::initialize_problem()
1309   {
1310     const unsigned int rods_per_assembly_x = 17, rods_per_assembly_y = 17;
1311     const double       pin_pitch_x = 1.26, pin_pitch_y = 1.26;
1312     const double       assembly_height = 200;
1313 
1314     const unsigned int assemblies_x = 2, assemblies_y = 2, assemblies_z = 1;
1315 
1316     const Point<dim> bottom_left = Point<dim>();
1317     const Point<dim> upper_right =
1318       (dim == 2 ? Point<dim>(assemblies_x * rods_per_assembly_x * pin_pitch_x,
1319                              assemblies_y * rods_per_assembly_y * pin_pitch_y) :
1320                   Point<dim>(assemblies_x * rods_per_assembly_x * pin_pitch_x,
1321                              assemblies_y * rods_per_assembly_y * pin_pitch_y,
1322                              assemblies_z * assembly_height));
1323 
1324     std::vector<unsigned int> n_subdivisions;
1325     n_subdivisions.push_back(assemblies_x * rods_per_assembly_x);
1326     if (dim >= 2)
1327       n_subdivisions.push_back(assemblies_y * rods_per_assembly_y);
1328     if (dim >= 3)
1329       n_subdivisions.push_back(assemblies_z);
1330 
1331     Triangulation<dim> coarse_grid;
1332     GridGenerator::subdivided_hyper_rectangle(
1333       coarse_grid, n_subdivisions, bottom_left, upper_right, true);
1334 
1335 
1336     // The second part of the function deals with material numbers of pin
1337     // cells of each type of assembly. Here, we define four different types of
1338     // assembly, for which we describe the arrangement of fuel rods in the
1339     // following tables.
1340     //
1341     // The assemblies described here are taken from the benchmark mentioned in
1342     // the introduction and are (in this order): <ol> <li>'UX' Assembly: UO2
1343     // fuel assembly with 24 guide tubes and a central Moveable Fission
1344     // Chamber <li>'UA' Assembly: UO2 fuel assembly with 24 AIC and a central
1345     // Moveable Fission Chamber <li>'PX' Assembly: MOX fuel assembly with 24
1346     // guide tubes and a central Moveable Fission Chamber <li>'R' Assembly: a
1347     // reflector.  </ol>
1348     //
1349     // Note that the numbers listed here and taken from the benchmark
1350     // description are, in good old Fortran fashion, one-based. We will later
1351     // subtract one from each number when assigning materials to individual
1352     // cells to convert things into the C-style zero-based indexing.
1353     const unsigned int n_assemblies = 4;
1354     const unsigned int assembly_materials
1355       [n_assemblies][rods_per_assembly_x][rods_per_assembly_y] = {
1356         {{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1357          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1358          {1, 1, 1, 1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1, 1, 1, 1},
1359          {1, 1, 1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 1, 1},
1360          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1361          {1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1},
1362          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1363          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1364          {1, 1, 5, 1, 1, 5, 1, 1, 7, 1, 1, 5, 1, 1, 5, 1, 1},
1365          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1366          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1367          {1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1},
1368          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1369          {1, 1, 1, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 1, 1, 1},
1370          {1, 1, 1, 1, 1, 5, 1, 1, 5, 1, 1, 5, 1, 1, 1, 1, 1},
1371          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1372          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}},
1373         {{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1374          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1375          {1, 1, 1, 1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1, 1, 1, 1},
1376          {1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1, 1},
1377          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1378          {1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1},
1379          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1380          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1381          {1, 1, 8, 1, 1, 8, 1, 1, 7, 1, 1, 8, 1, 1, 8, 1, 1},
1382          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1383          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1384          {1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1},
1385          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1386          {1, 1, 1, 8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 1, 1, 1},
1387          {1, 1, 1, 1, 1, 8, 1, 1, 8, 1, 1, 8, 1, 1, 1, 1, 1},
1388          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1},
1389          {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}},
1390         {{2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2},
1391          {2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2},
1392          {2, 3, 3, 3, 3, 5, 3, 3, 5, 3, 3, 5, 3, 3, 3, 3, 2},
1393          {2, 3, 3, 5, 3, 4, 4, 4, 4, 4, 4, 4, 3, 5, 3, 3, 2},
1394          {2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 2},
1395          {2, 3, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 3, 2},
1396          {2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 2},
1397          {2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 2},
1398          {2, 3, 5, 4, 4, 5, 4, 4, 7, 4, 4, 5, 4, 4, 5, 3, 2},
1399          {2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 2},
1400          {2, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 2},
1401          {2, 3, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 3, 2},
1402          {2, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 3, 3, 3, 2},
1403          {2, 3, 3, 5, 3, 4, 4, 4, 4, 4, 4, 4, 3, 5, 3, 3, 2},
1404          {2, 3, 3, 3, 3, 5, 3, 3, 5, 3, 3, 5, 3, 3, 3, 3, 2},
1405          {2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2},
1406          {2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2}},
1407         {{6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1408          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1409          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1410          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1411          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1412          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1413          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1414          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1415          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1416          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1417          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1418          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1419          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1420          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1421          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1422          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6},
1423          {6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6}}};
1424 
1425     // After the description of the materials that make up an assembly, we
1426     // have to specify the arrangement of assemblies within the core. We use a
1427     // symmetric pattern that in fact only uses the 'UX' and 'PX' assemblies:
1428     const unsigned int core[assemblies_x][assemblies_y][assemblies_z] = {
1429       {{0}, {2}}, {{2}, {0}}};
1430 
1431     // We are now in a position to actually set material IDs for each cell. To
1432     // this end, we loop over all cells, look at the location of the cell's
1433     // center, and determine which assembly and fuel rod this would be in. (We
1434     // add a few checks to see that the locations we compute are within the
1435     // bounds of the arrays in which we have to look up materials.) At the end
1436     // of the loop, we set material identifiers accordingly:
1437     for (auto &cell : coarse_grid.active_cell_iterators())
1438       {
1439         const Point<dim> cell_center = cell->center();
1440 
1441         const unsigned int tmp_x = int(cell_center[0] / pin_pitch_x);
1442         const unsigned int ax    = tmp_x / rods_per_assembly_x;
1443         const unsigned int cx    = tmp_x - ax * rods_per_assembly_x;
1444 
1445         const unsigned     tmp_y = int(cell_center[1] / pin_pitch_y);
1446         const unsigned int ay    = tmp_y / rods_per_assembly_y;
1447         const unsigned int cy    = tmp_y - ay * rods_per_assembly_y;
1448 
1449         const unsigned int az =
1450           (dim == 2 ? 0 : int(cell_center[dim - 1] / assembly_height));
1451 
1452         Assert(ax < assemblies_x, ExcInternalError());
1453         Assert(ay < assemblies_y, ExcInternalError());
1454         Assert(az < assemblies_z, ExcInternalError());
1455 
1456         Assert(core[ax][ay][az] < n_assemblies, ExcInternalError());
1457 
1458         Assert(cx < rods_per_assembly_x, ExcInternalError());
1459         Assert(cy < rods_per_assembly_y, ExcInternalError());
1460 
1461         cell->set_material_id(assembly_materials[core[ax][ay][az]][cx][cy] - 1);
1462       }
1463 
1464     // With the coarse mesh so initialized, we create the appropriate number
1465     // of energy group objects and let them initialize their individual meshes
1466     // with the coarse mesh generated above:
1467     for (unsigned int group = 0; group < parameters.n_groups; ++group)
1468       energy_groups.emplace_back(std::make_unique<EnergyGroup<dim>>(
1469         group, material_data, coarse_grid, fe));
1470     convergence_table_stream.open("convergence_table");
1471     convergence_table_stream.precision(12);
1472   }
1473 
1474 
1475   // @sect5{<code>NeutronDiffusionProblem::get_total_fission_source</code>}
1476   //
1477   // In the eigenvalue computation, we need to calculate total fission neutron
1478   // source after each power iteration. The total power then is used to renew
1479   // k-effective.
1480   //
1481   // Since the total fission source is a sum over all the energy groups, and
1482   // since each of these sums can be computed independently, we actually do
1483   // this in parallel. One of the problems is that the function in the
1484   // <code>EnergyGroup</code> class that computes the fission source returns a
1485   // value. We would like to add these values together in the loop itself:
1486   // ideally, each task would compute its value and then immediately add it to
1487   // the total. Concurrently summing values in this way requires two features:
1488   // <ol>
1489   //   <li>We need a way of storing a value such that multiple threads can
1490   //   read and write into concurrently in a way that prevents data races
1491   //   (i.e., thread-safe reading and writing).</li>
1492   //   <li>We need a way to increment such a value that is also
1493   //   thread-safe.</li>
1494   // </ol>
1495   //
1496   // The first feature is available through the template class
1497   // <code>std::atomic</code>. However, the second feature, implemented by
1498   // <code>std::atomic<double>::fetch_add()</code>, is only available in C++20
1499   // and later: since deal.II supports older versions of the C++ language
1500   // standard we cannot use this feature yet. Hence, instead, we simply write
1501   // each group's value out to an entry in a vector and sum the values at the
1502   // end of the function.
1503   template <int dim>
get_total_fission_source() const1504   double NeutronDiffusionProblem<dim>::get_total_fission_source() const
1505   {
1506     std::vector<double>  fission_sources(parameters.n_groups);
1507     Threads::TaskGroup<> tasks;
1508     for (unsigned int group = 0; group < parameters.n_groups; ++group)
1509       tasks += Threads::new_task<>([&, group]() {
1510         fission_sources[group] = energy_groups[group]->get_fission_source();
1511       });
1512     tasks.join_all();
1513 
1514     return std::accumulate(fission_sources.begin(), fission_sources.end(), 0.0);
1515   }
1516 
1517 
1518 
1519   // @sect5{<code>NeutronDiffusionProblem::refine_grid</code>}
1520   //
1521   // The next function lets the individual energy group objects refine their
1522   // meshes. Much of this, again, is a task that can be done independently in
1523   // parallel: first, let all the energy group objects calculate their error
1524   // indicators in parallel, then compute the maximum error indicator over all
1525   // energy groups and determine thresholds for refinement and coarsening of
1526   // cells, and then ask all the energy groups to refine their meshes
1527   // accordingly, again in parallel.
1528   template <int dim>
refine_grid()1529   void NeutronDiffusionProblem<dim>::refine_grid()
1530   {
1531     std::vector<types::global_dof_index> n_cells(parameters.n_groups);
1532     for (unsigned int group = 0; group < parameters.n_groups; ++group)
1533       n_cells[group] = energy_groups[group]->n_active_cells();
1534 
1535     BlockVector<float> group_error_indicators(n_cells);
1536 
1537     {
1538       Threads::TaskGroup<> tasks;
1539       for (unsigned int group = 0; group < parameters.n_groups; ++group)
1540         tasks += Threads::new_task([&, group]() {
1541           energy_groups[group]->estimate_errors(
1542             group_error_indicators.block(group));
1543         });
1544     }
1545     // The destructor of Threads::TaskGroup joins all threads so we know that
1546     // the computation is done by the time we exit the scope.
1547 
1548     const float max_error         = group_error_indicators.linfty_norm();
1549     const float refine_threshold  = 0.3 * max_error;
1550     const float coarsen_threshold = 0.01 * max_error;
1551 
1552     {
1553       Threads::TaskGroup<void> tasks;
1554       for (unsigned int group = 0; group < parameters.n_groups; ++group)
1555         tasks += Threads::new_task([&, group]() {
1556           energy_groups[group]->refine_grid(group_error_indicators.block(group),
1557                                             refine_threshold,
1558                                             coarsen_threshold);
1559         });
1560     }
1561   }
1562 
1563 
1564   // @sect5{<code>NeutronDiffusionProblem::run</code>}
1565   //
1566   // Finally, this is the function where the meat is: iterate on a sequence of
1567   // meshes, and on each of them do a power iteration to compute the
1568   // eigenvalue.
1569   //
1570   // Given the description of the algorithm in the introduction, there is
1571   // actually not much to comment on:
1572   template <int dim>
run()1573   void NeutronDiffusionProblem<dim>::run()
1574   {
1575     // We would like to change the output precision for just this function and
1576     // restore the state of <code>std::cout</code> when this function returns.
1577     // Hence, we need a way to undo the output format change. Boost provides a
1578     // convenient way to save the state of an output stream and restore it at
1579     // the end of the current block (when the destructor of
1580     // <code>restore_flags</code> is called) with the
1581     // <code>ios_flags_saver</code> class, which we use here.
1582     boost::io::ios_flags_saver restore_flags(std::cout);
1583     std::cout << std::setprecision(12) << std::fixed;
1584 
1585     // We calculate the error below by the change in k_eff (i.e., the
1586     // difference between k_eff_old,
1587     double k_eff_old = 0.0;
1588 
1589     for (unsigned int cycle = 0; cycle < parameters.n_refinement_cycles;
1590          ++cycle)
1591       {
1592         // We will measure the CPU time that each cycle takes below. The
1593         // constructor for Timer calls Timer::start(), so once we create a
1594         // timer we can query it for information. Since many parts of this
1595         // loop are parallelized with tasks, the CPU time we measure (if we
1596         // run with more than one thread) will be larger than the wall time.
1597         Timer timer;
1598 
1599         std::cout << "Cycle " << cycle << ':' << std::endl;
1600 
1601         if (cycle == 0)
1602           {
1603             initialize_problem();
1604             for (unsigned int group = 0; group < parameters.n_groups; ++group)
1605               energy_groups[group]->setup_linear_system();
1606           }
1607 
1608         else
1609           {
1610             refine_grid();
1611             for (unsigned int group = 0; group < parameters.n_groups; ++group)
1612               energy_groups[group]->solution *= k_eff;
1613           }
1614 
1615 
1616         std::cout << "   Numbers of active cells:       ";
1617         for (unsigned int group = 0; group < parameters.n_groups; ++group)
1618           std::cout << energy_groups[group]->n_active_cells() << ' ';
1619         std::cout << std::endl;
1620         std::cout << "   Numbers of degrees of freedom: ";
1621         for (unsigned int group = 0; group < parameters.n_groups; ++group)
1622           std::cout << energy_groups[group]->n_dofs() << ' ';
1623         std::cout << std::endl << std::endl;
1624 
1625         Threads::TaskGroup<> tasks;
1626         for (unsigned int group = 0; group < parameters.n_groups; ++group)
1627           tasks += Threads::new_task(
1628             [&, group]() { energy_groups[group]->assemble_system_matrix(); });
1629         tasks.join_all();
1630 
1631         double       error;
1632         unsigned int iteration = 1;
1633         do
1634           {
1635             for (unsigned int group = 0; group < parameters.n_groups; ++group)
1636               {
1637                 energy_groups[group]->assemble_ingroup_rhs(
1638                   Functions::ZeroFunction<dim>());
1639 
1640                 for (unsigned int bgroup = 0; bgroup < parameters.n_groups;
1641                      ++bgroup)
1642                   energy_groups[group]->assemble_cross_group_rhs(
1643                     *energy_groups[bgroup]);
1644 
1645                 energy_groups[group]->solve();
1646               }
1647 
1648             k_eff = get_total_fission_source();
1649             error = std::abs(k_eff - k_eff_old) / std::abs(k_eff);
1650             const double flux_ratio = energy_groups[0]->solution.linfty_norm() /
1651                                       energy_groups[1]->solution.linfty_norm();
1652             const double max_thermal = energy_groups[1]->solution.linfty_norm();
1653             std::cout << "Iter number:" << std::setw(2) << std::right
1654                       << iteration << " k_eff=" << k_eff
1655                       << " flux_ratio=" << flux_ratio
1656                       << " max_thermal=" << max_thermal << std::endl;
1657             k_eff_old = k_eff;
1658 
1659             for (unsigned int group = 0; group < parameters.n_groups; ++group)
1660               {
1661                 energy_groups[group]->solution_old =
1662                   energy_groups[group]->solution;
1663                 energy_groups[group]->solution_old /= k_eff;
1664               }
1665 
1666             ++iteration;
1667           }
1668         while ((error > parameters.convergence_tolerance) && (iteration < 500));
1669         convergence_table_stream << cycle << " " << energy_groups[0]->n_dofs()
1670                                  << " " << energy_groups[1]->n_dofs() << " "
1671                                  << k_eff << " "
1672                                  << energy_groups[0]->solution.linfty_norm() /
1673                                       energy_groups[1]->solution.linfty_norm()
1674                                  << '\n';
1675 
1676         for (unsigned int group = 0; group < parameters.n_groups; ++group)
1677           energy_groups[group]->output_results(cycle);
1678 
1679         // Print out information about the simulation as well as the elapsed
1680         // CPU time. We can call Timer::cpu_time() without first calling
1681         // Timer::stop() to get the elapsed CPU time at the point of calling
1682         // the function.
1683         std::cout << std::endl;
1684         std::cout << "   Cycle=" << cycle << ", n_dofs="
1685                   << energy_groups[0]->n_dofs() + energy_groups[1]->n_dofs()
1686                   << ",  k_eff=" << k_eff << ", time=" << timer.cpu_time()
1687                   << std::endl;
1688 
1689 
1690         std::cout << std::endl << std::endl;
1691       }
1692   }
1693 } // namespace Step28
1694 
1695 
1696 
1697 // @sect3{The <code>main()</code> function}
1698 //
1699 // The last thing in the program in the <code>main()</code> function. The
1700 // structure is as in most other tutorial programs, with the only exception
1701 // that we here handle a parameter file.  To this end, we first look at the
1702 // command line arguments passed to this function: if no input file is
1703 // specified on the command line, then use "project.prm", otherwise take the
1704 // filename given as the first argument on the command line.
1705 //
1706 // With this, we create a ParameterHandler object, let the
1707 // <code>NeutronDiffusionProblem::Parameters</code> class declare all the
1708 // parameters it wants to see in the input file (or, take the default values,
1709 // if nothing is listed in the parameter file), then read the input file, ask
1710 // the parameters object to extract the values, and finally hand everything
1711 // off to an object of type <code>NeutronDiffusionProblem</code> for
1712 // computation of the eigenvalue:
main(int argc,char ** argv)1713 int main(int argc, char **argv)
1714 {
1715   try
1716     {
1717       using namespace dealii;
1718       using namespace Step28;
1719 
1720       std::string filename;
1721       if (argc < 2)
1722         filename = "project.prm";
1723       else
1724         filename = argv[1];
1725 
1726 
1727       const unsigned int dim = 2;
1728 
1729       ParameterHandler parameter_handler;
1730 
1731       NeutronDiffusionProblem<dim>::Parameters parameters;
1732       parameters.declare_parameters(parameter_handler);
1733 
1734       parameter_handler.parse_input(filename);
1735 
1736       parameters.get_parameters(parameter_handler);
1737 
1738 
1739       NeutronDiffusionProblem<dim> neutron_diffusion_problem(parameters);
1740       neutron_diffusion_problem.run();
1741     }
1742   catch (std::exception &exc)
1743     {
1744       std::cerr << std::endl
1745                 << std::endl
1746                 << "----------------------------------------------------"
1747                 << std::endl;
1748       std::cerr << "Exception on processing: " << std::endl
1749                 << exc.what() << std::endl
1750                 << "Aborting!" << std::endl
1751                 << "----------------------------------------------------"
1752                 << std::endl;
1753 
1754       return 1;
1755     }
1756   catch (...)
1757     {
1758       std::cerr << std::endl
1759                 << std::endl
1760                 << "----------------------------------------------------"
1761                 << std::endl;
1762       std::cerr << "Unknown exception!" << std::endl
1763                 << "Aborting!" << std::endl
1764                 << "----------------------------------------------------"
1765                 << std::endl;
1766       return 1;
1767     }
1768 
1769   return 0;
1770 }
1771