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 ¶meters);
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 ¶meters)
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