-
Notifications
You must be signed in to change notification settings - Fork 708
/
step-19.cc
1084 lines (918 loc) · 44.4 KB
/
step-19.cc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* ---------------------------------------------------------------------
*
* Copyright (C) 2020 - 2021 by the deal.II authors
*
* This file is part of the deal.II library.
*
* The deal.II library is free software; you can use it, redistribute
* it, and/or modify it under the terms of the GNU Lesser General
* Public License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* The full text of the license can be found in the file LICENSE.md at
* the top level directory of deal.II.
*
* ---------------------------------------------------------------------
*
* Authors: Wolfgang Bangerth, Rene Gassmoeller, Peter Munch, 2020.
*/
// @sect3{Include files}
// The majority of the include files used in this program are
// well known from step-6 and similar programs:
#include <deal.II/base/quadrature_lib.h>
#include <deal.II/lac/dynamic_sparsity_pattern.h>
#include <deal.II/lac/full_matrix.h>
#include <deal.II/lac/precondition.h>
#include <deal.II/lac/solver_cg.h>
#include <deal.II/lac/sparse_matrix.h>
#include <deal.II/lac/vector.h>
#include <deal.II/lac/affine_constraints.h>
#include <deal.II/grid/tria.h>
#include <deal.II/grid/grid_refinement.h>
#include <deal.II/fe/mapping_q.h>
#include <deal.II/matrix_free/fe_point_evaluation.h>
#include <deal.II/fe/fe_q.h>
#include <deal.II/fe/fe_values.h>
#include <deal.II/dofs/dof_handler.h>
#include <deal.II/dofs/dof_tools.h>
#include <deal.II/numerics/data_out.h>
#include <deal.II/numerics/vector_tools.h>
#include <deal.II/numerics/error_estimator.h>
// The ones that are new are only the following three: The first declares the
// DiscreteTime class that helps us keep track of time in a time-dependent
// simulation. The latter two provide all of the particle functionality,
// namely a way to keep track of particles located on a mesh (the
// Particles::ParticleHandler class) and the ability to output these
// particles' locations and their properties for the purposes of
// visualization (the Particles::DataOut class).
#include <deal.II/base/discrete_time.h>
#include <deal.II/particles/particle_handler.h>
#include <deal.II/particles/data_out.h>
#include <fstream>
using namespace dealii;
// @sect3{Global definitions}
// As is customary, we put everything that corresponds to the details of the
// program into a namespace of its own. At the top, we define a few constants
// for which we would rather use symbolic names than hard-coded numbers.
//
// Specifically, we define numbers for
// @ref GlossBoundaryIndicator "boundary indicators"
// for the various parts of the geometry, as well as the physical properties
// of electrons and other specifics of the setup we use here.
//
// For the boundary indicators, let us start enumerating at some
// random value 101. The principle here is to use numbers that are
// *uncommon*. If there are pre-defined boundary indicators previously
// set by the `GridGenerator` functions, they will likely be small
// integers starting from zero, but not in this rather randomly chosen
// range. Using numbers such as those below avoids the possibility for
// conflicts, and also reduces the temptation to just spell these
// numbers out in the program (because you will probably never
// remember which is which, whereas you might have been tempted if
// they had started at 0).
namespace Step19
{
namespace BoundaryIds
{
constexpr types::boundary_id open = 101;
constexpr types::boundary_id cathode = 102;
constexpr types::boundary_id focus_element = 103;
constexpr types::boundary_id anode = 104;
} // namespace BoundaryIds
namespace Constants
{
constexpr double electron_mass = 9.1093837015e-31;
constexpr double electron_charge = 1.602176634e-19;
constexpr double V0 = 1;
constexpr double E_threshold = 0.05;
constexpr double electrons_per_particle = 3e15;
} // namespace Constants
// @sect3{The main class}
// The following is then the main class of this program. It has,
// fundamentally, the same structure as step-6 and many other
// tutorial programs. This includes the majority of the member
// functions (with the purpose of the rest probably self-explanatory
// from their names) as well as only a small number of member
// variables beyond those of step-6, all of which are related to
// dealing with particles.
template <int dim>
class CathodeRaySimulator
{
public:
CathodeRaySimulator();
void run();
private:
void make_grid();
void setup_system();
void assemble_system();
void solve_field();
void refine_grid();
void create_particles();
void move_particles();
void track_lost_particle(
const typename Particles::ParticleIterator<dim> & particle,
const typename Triangulation<dim>::active_cell_iterator &cell);
void update_timestep_size();
void output_results() const;
Triangulation<dim> triangulation;
MappingQ<dim> mapping;
FE_Q<dim> fe;
DoFHandler<dim> dof_handler;
AffineConstraints<double> constraints;
SparseMatrix<double> system_matrix;
SparsityPattern sparsity_pattern;
Vector<double> solution;
Vector<double> system_rhs;
Particles::ParticleHandler<dim> particle_handler;
types::particle_index next_unused_particle_id;
types::particle_index n_recently_lost_particles;
types::particle_index n_total_lost_particles;
types::particle_index n_particles_lost_through_anode;
DiscreteTime time;
};
// @sect3{The <code>CathodeRaySimulator</code> class implementation}
// @sect4{The <code>CathodeRaySimulator</code> constructor}
// So then let us get started on the implementation. What the constructor
// does is really only a straight-forward initialization of all of the member
// variables at the top. The only two worth mentioning are the
// `particle_handler`, which is handed a reference to the triangulation
// on which the particles will live (currently of course still empty,
// but the particle handler stores the reference and will use it once
// particles are added -- which happens after the triangulation is built).
// The other piece of information it gets is how many "properties"
// each particle needs to store. Here, all we need each particle to
// remember is its current velocity, i.e., a vector with `dim`
// components. There are, however, other intrinsic properties that
// each particle has and that the Particles::ParticleHandler class
// automatically and always makes sure are available; in particular,
// these are the current location of a particle, the cell it is on,
// it's reference location within that cell, and the particle's ID.
//
// The only other variable of interest is `time`, an object of type
// DiscreteTime. It keeps track of the current time we are in a
// time-dependent simulation, and is initialized with the start time
// (zero) and end time ($10^{-4}$). We will later set the time step
// size in `update_timestep_size()`.
//
// The body of the constructor consists of a piece of code we have
// already discussed in the introduction. Namely, we make sure that the
// `track_lost_particle()` function is called by the `particle_handler`
// object every time a particle leaves the domain.
template <int dim>
CathodeRaySimulator<dim>::CathodeRaySimulator()
: mapping(1)
, fe(2)
, dof_handler(triangulation)
, particle_handler(triangulation, mapping, /*n_properties=*/dim)
, next_unused_particle_id(0)
, n_recently_lost_particles(0)
, n_total_lost_particles(0)
, n_particles_lost_through_anode(0)
, time(0, 1e-4)
{
particle_handler.signals.particle_lost.connect(
[this](const typename Particles::ParticleIterator<dim> & particle,
const typename Triangulation<dim>::active_cell_iterator &cell) {
this->track_lost_particle(particle, cell);
});
}
// @sect4{The <code>CathodeRaySimulator::make_grid</code> function}
// The next function is then responsible for generating the mesh on which
// we want to solve. Recall how the domain looks like:
// <p align="center">
// <img
// src="https://www.dealii.org/images/steps/developer/step-19.geometry.png"
// alt="The geometry used in this program"
// width="600">
// </p>
// We subdivide this geometry into a mesh of $4\times 2$ cells that looks
// like this:
// @code
// *---*---*---*---*
// \ | | | |
// *--*---*---*---*
// / | | | |
// *---*---*---*---*
// @endcode
// The way this is done is by first defining where the $15=5\times 3$
// vertices are located -- here, we say that they are on integer points
// with the middle one on the left side moved to the right by a value of
// `delta=0.5`.
//
// In the following, we then have to say which vertices together form
// the 8 cells. The following code is then entirely equivalent to what
// we also do in step-14:
template <int dim>
void CathodeRaySimulator<dim>::make_grid()
{
static_assert(dim == 2,
"This function is currently only implemented for 2d.");
const double delta = 0.5;
const unsigned int nx = 5;
const unsigned int ny = 3;
const std::vector<Point<dim>> vertices //
= {{0, 0},
{1, 0},
{2, 0},
{3, 0},
{4, 0},
{delta, 1},
{1, 1},
{2, 1},
{3, 1},
{4, 1},
{0, 2},
{1, 2},
{2, 2},
{3, 2},
{4, 2}};
AssertDimension(vertices.size(), nx * ny);
const std::vector<unsigned int> cell_vertices[(nx - 1) * (ny - 1)] = {
{0, 1, nx + 0, nx + 1},
{1, 2, nx + 1, nx + 2},
{2, 3, nx + 2, nx + 3},
{3, 4, nx + 3, nx + 4},
{5, nx + 1, 2 * nx + 0, 2 * nx + 1},
{nx + 1, nx + 2, 2 * nx + 1, 2 * nx + 2},
{nx + 2, nx + 3, 2 * nx + 2, 2 * nx + 3},
{nx + 3, nx + 4, 2 * nx + 3, 2 * nx + 4}};
// With these arrays out of the way, we can move to slightly higher
// higher-level data structures. We create a vector of CellData
// objects that store for each cell to be created the vertices in
// question as well as the @ref GlossMaterialId "material id" (which
// we will here simply set to zero since we don't use it in the program).
//
// This information is then handed to the
// Triangulation::create_triangulation() function, and the mesh is twice
// globally refined.
std::vector<CellData<dim>> cells((nx - 1) * (ny - 1), CellData<dim>());
for (unsigned int i = 0; i < cells.size(); ++i)
{
cells[i].vertices = cell_vertices[i];
cells[i].material_id = 0;
}
triangulation.create_triangulation(
vertices,
cells,
SubCellData()); // No boundary information
triangulation.refine_global(2);
// The remaining part of the function loops over all cells and their faces,
// and if a face is at the boundary determines which boundary indicator
// should be applied to it. The various conditions should make sense if
// you compare the code with the picture of the geometry above.
//
// Once done with this step, we refine the mesh once more globally.
for (auto &cell : triangulation.active_cell_iterators())
for (auto &face : cell->face_iterators())
if (face->at_boundary())
{
if ((face->center()[0] > 0) && (face->center()[0] < 0.5) &&
(face->center()[1] > 0) && (face->center()[1] < 2))
face->set_boundary_id(BoundaryIds::cathode);
else if ((face->center()[0] > 0) && (face->center()[0] < 2))
face->set_boundary_id(BoundaryIds::focus_element);
else if ((face->center()[0] > 4 - 1e-12) &&
((face->center()[1] > 1.5) || (face->center()[1] < 0.5)))
face->set_boundary_id(BoundaryIds::anode);
else
face->set_boundary_id(BoundaryIds::open);
}
triangulation.refine_global(1);
}
// @sect4{The <code>CathodeRaySimulator::setup_system</code> function}
// The next function in this program deals with setting up the various
// objects related to solving the partial differential equations. It is
// in essence a copy of the corresponding function in step-6 and requires
// no further discussion.
template <int dim>
void CathodeRaySimulator<dim>::setup_system()
{
dof_handler.distribute_dofs(fe);
solution.reinit(dof_handler.n_dofs());
system_rhs.reinit(dof_handler.n_dofs());
constraints.clear();
DoFTools::make_hanging_node_constraints(dof_handler, constraints);
VectorTools::interpolate_boundary_values(dof_handler,
BoundaryIds::cathode,
Functions::ConstantFunction<dim>(
-Constants::V0),
constraints);
VectorTools::interpolate_boundary_values(dof_handler,
BoundaryIds::focus_element,
Functions::ConstantFunction<dim>(
-Constants::V0),
constraints);
VectorTools::interpolate_boundary_values(dof_handler,
BoundaryIds::anode,
Functions::ConstantFunction<dim>(
+Constants::V0),
constraints);
constraints.close();
DynamicSparsityPattern dsp(dof_handler.n_dofs());
DoFTools::make_sparsity_pattern(dof_handler,
dsp,
constraints,
/*keep_constrained_dofs = */ false);
sparsity_pattern.copy_from(dsp);
system_matrix.reinit(sparsity_pattern);
}
// @sect4{The <code>CathodeRaySimulator::assemble_system</code> function}
// The function that computes
// the matrix entries is again in essence a copy of the
// corresponding function in step-6:
template <int dim>
void CathodeRaySimulator<dim>::assemble_system()
{
system_matrix = 0;
system_rhs = 0;
const QGauss<dim> quadrature_formula(fe.degree + 1);
FEValues<dim> fe_values(fe,
quadrature_formula,
update_values | update_gradients |
update_quadrature_points | update_JxW_values);
const unsigned int dofs_per_cell = fe.dofs_per_cell;
FullMatrix<double> cell_matrix(dofs_per_cell, dofs_per_cell);
Vector<double> cell_rhs(dofs_per_cell);
std::vector<types::global_dof_index> local_dof_indices(dofs_per_cell);
for (const auto &cell : dof_handler.active_cell_iterators())
{
cell_matrix = 0;
cell_rhs = 0;
fe_values.reinit(cell);
for (const unsigned int q_index : fe_values.quadrature_point_indices())
for (const unsigned int i : fe_values.dof_indices())
{
for (const unsigned int j : fe_values.dof_indices())
cell_matrix(i, j) +=
(fe_values.shape_grad(i, q_index) * // grad phi_i(x_q)
fe_values.shape_grad(j, q_index) * // grad phi_j(x_q)
fe_values.JxW(q_index)); // dx
}
// The only interesting part of this function is how it forms the right
// hand side of the linear system. Recall that the right hand side
// of the PDE is
// @f[
// \sum_p (N e)\delta(\mathbf x-\mathbf x_p),
// @f]
// where we have used $p$ to index the particles here to avoid
// confusion with the shape function $\varphi_i$; $\mathbf x_p$
// is the position of the $p$th particle.
//
// When multiplied by a test function $\varphi_i$ and integrated over
// the domain results in a right hand side vector
// @f{align*}{
// F_i &= \int_\Omega \varphi_i (\mathbf x)\left[
// \sum_p (N e)\delta(\mathbf x-\mathbf x_p) \right] dx
// \\ &= \sum_p (N e) \varphi_i(\mathbf x_p).
// @f}
// Note that the final line no longer contains an integral, and
// consequently also no occurrence of $dx$ which would require the
// appearance of the `JxW` symbol in our code.
//
// For a given cell $K$, this cell's contribution to the right hand
// side is then
// @f{align*}{
// F_i^K &= \sum_{p, \mathbf x_p\in K} (N e) \varphi_i(\mathbf x_p),
// @f}
// i.e., we only have to worry about those particles that are actually
// located on the current cell $K$.
//
// In practice, what we do here is the following: If there are any
// particles on the current cell, then we first obtain an iterator range
// pointing to the first particle of that cell as well as the particle
// past the last one on this cell (or the end iterator) -- i.e., a
// half-open range as is common for C++ functions. Knowing now the list
// of particles, we query their reference locations (with respect to
// the reference cell), evaluate the shape functions in these reference
// locations, and compute the force according to the formula above
// (without any FEValues::JxW).
//
// @note It is worth pointing out that calling the
// Particles::ParticleHandler::particles_in_cell() and
// Particles::ParticleHandler::n_particles_in_cell() functions is not
// very efficient on problems with a large number of particles. But it
// illustrates the easiest way to write this algorithm, and so we are
// willing to incur this cost for the moment for expository purposes.
// We discuss the issue in more detail in the
// <a href="#extensions">"possibilities for extensions" section</a>
// below, and use a better approach in step-70, for example.
if (particle_handler.n_particles_in_cell(cell) > 0)
for (const auto &particle : particle_handler.particles_in_cell(cell))
{
const Point<dim> &reference_location =
particle.get_reference_location();
for (const unsigned int i : fe_values.dof_indices())
cell_rhs(i) +=
(fe.shape_value(i, reference_location) * // phi_i(x_p)
(-Constants::electrons_per_particle * // N
Constants::electron_charge)); // e
}
// Finally, we can copy the contributions of this cell into
// the global matrix and right hand side vector:
cell->get_dof_indices(local_dof_indices);
constraints.distribute_local_to_global(
cell_matrix, cell_rhs, local_dof_indices, system_matrix, system_rhs);
}
}
// @sect4{CathodeRaySimulator::solve}
// The function that solves the linear system is then again exactly as in
// step-6:
template <int dim>
void CathodeRaySimulator<dim>::solve_field()
{
SolverControl solver_control(1000, 1e-12);
SolverCG<Vector<double>> solver(solver_control);
PreconditionSSOR<SparseMatrix<double>> preconditioner;
preconditioner.initialize(system_matrix, 1.2);
solver.solve(system_matrix, solution, system_rhs, preconditioner);
constraints.distribute(solution);
}
// @sect4{CathodeRaySimulator::refine_grid}
// The final field-related function is the one that refines the grid. We will
// call it a number of times in the first time step to obtain a mesh that
// is well-adapted to the structure of the solution and, in particular,
// resolves the various singularities in the solution that are due to
// re-entrant corners and places where the boundary condition type
// changes. You might want to refer to step-6 again for more details:
template <int dim>
void CathodeRaySimulator<dim>::refine_grid()
{
Vector<float> estimated_error_per_cell(triangulation.n_active_cells());
KellyErrorEstimator<dim>::estimate(dof_handler,
QGauss<dim - 1>(fe.degree + 1),
{},
solution,
estimated_error_per_cell);
GridRefinement::refine_and_coarsen_fixed_number(triangulation,
estimated_error_per_cell,
0.1,
0.03);
triangulation.execute_coarsening_and_refinement();
}
// @sect4{CathodeRaySimulator::create_particles}
// Let us now turn to the functions that deal with particles. The first one
// is about the creation of particles. As mentioned in the introduction,
// we want to create a particle at points of the cathode if the the electric
// field $\mathbf E=\nabla V$ exceeds a certain threshold, i.e., if
// $|\mathbf E| \ge E_\text{threshold}$, and if furthermore the electric field
// points into the domain (i.e., if $\mathbf E \cdot \mathbf n < 0$). As is
// common in the finite element method, we evaluate fields (and their
// derivatives) at specific evaluation points; typically, these are
// "quadrature points", and so we create a "quadrature formula" that we will
// use to designate the points at which we want to evaluate the solution.
// Here, we will simply take QMidpoint implying that we will only check the
// threshold condition at the midpoints of faces. We then use this to
// initialize an object of type FEFaceValues to evaluate the solution at these
// points.
//
// All of this will then be used in a loop over all cells, their faces, and
// specifically those faces that are at the boundary and, moreover, the
// cathode part of the boundary.
template <int dim>
void CathodeRaySimulator<dim>::create_particles()
{
FEFaceValues<dim> fe_face_values(fe,
QMidpoint<dim - 1>(),
update_quadrature_points |
update_gradients |
update_normal_vectors);
std::vector<Tensor<1, dim>> solution_gradients(
fe_face_values.n_quadrature_points);
for (const auto &cell : dof_handler.active_cell_iterators())
for (const auto &face : cell->face_iterators())
if (face->at_boundary() &&
(face->boundary_id() == BoundaryIds::cathode))
{
fe_face_values.reinit(cell, face);
// So we have found a face on the cathode. Next, we let the
// FEFaceValues object compute the gradient of the solution at each
// "quadrature" point, and extract the electric field vector from
// the gradient in the form of a Tensor variable through the methods
// discussed in the
// @ref vector_valued "vector-valued problems" documentation module.
const FEValuesExtractors::Scalar electric_potential(0);
fe_face_values[electric_potential].get_function_gradients(
solution, solution_gradients);
for (const unsigned int q_point :
fe_face_values.quadrature_point_indices())
{
const Tensor<1, dim> E = solution_gradients[q_point];
// Electrons can only escape the cathode if the electric field
// strength exceeds a threshold and,
// crucially, if the electric field points *into* the domain.
// Once we have that checked, we create a new
// Particles::Particle object at this location and insert it
// into the Particles::ParticleHandler object with a unique ID.
//
// The only thing that may be not obvious here is that we also
// associate with this particle the location in the reference
// coordinates of the cell we are currently on. This is done
// because we will in downstream functions compute quantities
// such as the electric field at the location of the particle
// (e.g., to compute the forces that act on it when updating its
// position in each time step). Evaluating a finite element
// field at arbitrary coordinates is quite an expensive
// operation because shape functions are really only defined on
// the reference cell, and so when asking for the electric field
// at an arbitrary point requires us first to determine what the
// reference coordinates of that point are. To avoid having to
// do this over and over, we determine these coordinates once
// and for all and then store these reference coordinates
// directly with the particle.
if ((E * fe_face_values.normal_vector(q_point) < 0) &&
(E.norm() > Constants::E_threshold))
{
const Point<dim> &location =
fe_face_values.quadrature_point(q_point);
Particles::Particle<dim> new_particle;
new_particle.set_location(location);
new_particle.set_reference_location(
mapping.transform_real_to_unit_cell(cell, location));
new_particle.set_id(next_unused_particle_id);
particle_handler.insert_particle(new_particle, cell);
++next_unused_particle_id;
}
}
}
// At the end of all of these insertions, we let the `particle_handler`
// update some internal statistics about the particles it stores.
particle_handler.update_cached_numbers();
}
// @sect4{CathodeRaySimulator::move_particles}
// The second particle-related function is the one that moves the particles
// in each time step. To do this, we have to loop over all cells, the
// particles in each cell, and evaluate the electric field at each of the
// particles' positions.
//
// The approach used here is conceptually the same used in the
// `assemble_system()` function: We loop over all cells, find the particles
// located there (with the same caveat about the inefficiency of the algorithm
// used here to find these particles), and use FEPointEvaluation object to
// evaluate the gradient at these positions:
template <int dim>
void CathodeRaySimulator<dim>::move_particles()
{
const double dt = time.get_next_step_size();
Vector<double> solution_values(fe.n_dofs_per_cell());
FEPointEvaluation<1, dim> evaluator(mapping, fe, update_gradients);
for (const auto &cell : dof_handler.active_cell_iterators())
if (particle_handler.n_particles_in_cell(cell) > 0)
{
const typename Particles::ParticleHandler<
dim>::particle_iterator_range particles_in_cell =
particle_handler.particles_in_cell(cell);
std::vector<Point<dim>> particle_positions;
for (const auto &particle : particles_in_cell)
particle_positions.push_back(particle.get_reference_location());
cell->get_dof_values(solution, solution_values);
// Then we can ask the FEPointEvaluation object for the gradients of
// the solution (i.e., the electric field $\mathbf E$) at these
// locations and loop over the individual particles:
evaluator.reinit(cell, particle_positions);
evaluator.evaluate(make_array_view(solution_values),
EvaluationFlags::gradients);
{
typename Particles::ParticleHandler<dim>::particle_iterator
particle = particles_in_cell.begin();
for (unsigned int particle_index = 0;
particle != particles_in_cell.end();
++particle, ++particle_index)
{
const Tensor<1, dim> &E =
evaluator.get_gradient(particle_index);
// Having now obtained the electric field at the location of one
// of the particles, we use this to update first the velocity
// and then the position. To do so, let us first get the old
// velocity out of the properties stored with the particle,
// compute the acceleration, update the velocity, and store this
// new velocity again in the properties of the particle. Recall
// that this corresponds to the first of the following set of
// update equations discussed in the introduction:
// @f{align*}{
// \frac{{\mathbf v}_i^{(n)}
// -{\mathbf v}_i^{(n-1)}}{\Delta t}
// &= \frac{e\nabla V^{(n)}}{m}
// \\ \frac{{\mathbf x}_i^{(n)}-{\mathbf x}_i^{(n-1)}}
// {\Delta t} &= {\mathbf v}_i^{(n)}.
// @f}
const Tensor<1, dim> old_velocity(particle->get_properties());
const Tensor<1, dim> acceleration =
Constants::electron_charge / Constants::electron_mass * E;
const Tensor<1, dim> new_velocity =
old_velocity + acceleration * dt;
particle->set_properties(new_velocity);
// With the new velocity, we can then also update the location
// of the particle and tell the particle about it.
const Point<dim> new_location =
particle->get_location() + dt * new_velocity;
particle->set_location(new_location);
}
}
}
// Having updated the locations and properties (i.e., velocities) of all
// particles, we need to make sure that the `particle_handler` again knows
// which cells they are in, and what their locations in the coordinate
// system of the reference cell are. The following function does that. (It
// also makes sure that, in parallel computations, particles are moved from
// one processor to another processor if a particle moves from the subdomain
// owned by the former to the subdomain owned by the latter.)
particle_handler.sort_particles_into_subdomains_and_cells();
}
// @sect4{CathodeRaySimulator::track_lost_particle}
// The final particle-related function is the one that is called whenever a
// particle is lost from the simulation. This typically happens if it leaves
// the domain. If that happens, this function is called both the cell (which
// we can ask for its new location) and the cell it was previously on. The
// function then keeps track of updating the number of particles lost in this
// time step, the total number of lost particles, and then estimates whether
// the particle left through the hole in the middle of the anode. We do so by
// first checking whether the cell it was in last had an $x$ coordinate to the
// left of the right boundary (located at $x=4$) and the particle now has a
// position to the right of the right boundary. If that is so, we compute a
// direction vector of its motion that is normalized so that the $x$ component
// of the direction vector is equal to $1$. With this direction vector, we can
// compute where it would have intersected the line $x=4$. If this intersect
// is between $0.5$ and $1.5$, then we claim that the particle left through
// the hole and increment a counter.
template <int dim>
void CathodeRaySimulator<dim>::track_lost_particle(
const typename Particles::ParticleIterator<dim> & particle,
const typename Triangulation<dim>::active_cell_iterator &cell)
{
++n_recently_lost_particles;
++n_total_lost_particles;
const Point<dim> current_location = particle->get_location();
const Point<dim> approximate_previous_location = cell->center();
if ((approximate_previous_location[0] < 4) && (current_location[0] > 4))
{
const Tensor<1, dim> direction =
(current_location - approximate_previous_location) /
(current_location[0] - approximate_previous_location[0]);
const double right_boundary_intercept =
approximate_previous_location[1] +
(4 - approximate_previous_location[0]) * direction[1];
if ((right_boundary_intercept > 0.5) &&
(right_boundary_intercept < 1.5))
++n_particles_lost_through_anode;
}
}
// @sect4{CathodeRaySimulator::update_timestep_size}
// As discussed at length in the introduction, we need to respect a time step
// condition whereby particles can not move further than one cell in one time
// step. To ensure that this is the case, we again first compute the maximal
// speed of all particles on each cell, and divide the cell size by that
// speed. We then compute the next time step size as the minimum of this
// quantity over all cells, using the safety factor discussed in the
// introduction, and set this as the desired time step size using the
// DiscreteTime::set_desired_time_step_size() function.
template <int dim>
void CathodeRaySimulator<dim>::update_timestep_size()
{
if (time.get_step_number() > 0)
{
double min_cell_size_over_velocity = std::numeric_limits<double>::max();
for (const auto &cell : dof_handler.active_cell_iterators())
if (particle_handler.n_particles_in_cell(cell) > 0)
{
const double cell_size = cell->minimum_vertex_distance();
double max_particle_velocity(0.0);
for (const auto &particle :
particle_handler.particles_in_cell(cell))
{
const Tensor<1, dim> velocity(particle.get_properties());
max_particle_velocity =
std::max(max_particle_velocity, velocity.norm());
}
if (max_particle_velocity > 0)
min_cell_size_over_velocity =
std::min(min_cell_size_over_velocity,
cell_size / max_particle_velocity);
}
constexpr double c_safety = 0.5;
time.set_desired_next_step_size(c_safety * 0.5 *
min_cell_size_over_velocity);
}
// As mentioned in the introduction, we have to treat the very first
// time step differently since there, particles are not available yet or
// do not yet have the information associated that we need for the
// computation of a reasonable step length. The formulas below follow the
// discussion in the introduction.
else
{
const QTrapezoid<dim> vertex_quadrature;
FEValues<dim> fe_values(fe, vertex_quadrature, update_gradients);
std::vector<Tensor<1, dim>> field_gradients(vertex_quadrature.size());
double min_timestep = std::numeric_limits<double>::max();
for (const auto &cell : dof_handler.active_cell_iterators())
if (particle_handler.n_particles_in_cell(cell) > 0)
{
const double cell_size = cell->minimum_vertex_distance();
fe_values.reinit(cell);
fe_values.get_function_gradients(solution, field_gradients);
double max_E = 0;
for (const auto q_point : fe_values.quadrature_point_indices())
max_E = std::max(max_E, field_gradients[q_point].norm());
if (max_E > 0)
min_timestep =
std::min(min_timestep,
std::sqrt(0.5 * cell_size *
Constants::electron_mass /
Constants::electron_charge / max_E));
}
time.set_desired_next_step_size(min_timestep);
}
}
// @sect4{The <code>CathodeRaySimulator::output_results()</code> function}
// The final function implementing pieces of the overall algorithm is the one
// that generates graphical output. In the current context, we want to output
// both the electric potential field as well as the particle locations and
// velocities. But we also want to output the electric field, i.e., the
// gradient of the solution.
//
// deal.II has a general way how one can compute derived quantities from
// the solution and output those as well. Here, this is the electric
// field, but it could also be some other quantity -- say, the norm of the
// electric field, or in fact anything else one could want to compute from
// the solution $V_h(\mathbf x)$ or its derivatives. This general solution
// uses the DataPostprocessor class and, in cases like the one here where we
// want to output a quantity that represents a vector field, the
// DataPostprocessorVector class.
//
// Rather than try and explain how this class works, let us simply refer to
// the documentation of the DataPostprocessorVector class that has essentially
// this case as a well-documented example.
template <int dim>
class ElectricFieldPostprocessor : public DataPostprocessorVector<dim>
{
public:
ElectricFieldPostprocessor()
: DataPostprocessorVector<dim>("electric_field", update_gradients)
{}
virtual void evaluate_scalar_field(
const DataPostprocessorInputs::Scalar<dim> &input_data,
std::vector<Vector<double>> &computed_quantities) const override
{
AssertDimension(input_data.solution_gradients.size(),
computed_quantities.size());
for (unsigned int p = 0; p < input_data.solution_gradients.size(); ++p)
{
AssertDimension(computed_quantities[p].size(), dim);
for (unsigned int d = 0; d < dim; ++d)
computed_quantities[p][d] = input_data.solution_gradients[p][d];
}
}
};
// With this, the `output_results()` function becomes relatively
// straightforward: We use the DataOut class as we have in almost
// every one of the previous tutorial programs to output the
// solution (the "electric potential") and we use the postprocessor
// defined above to also output its gradient (the "electric
// field"). This all is then written into a file in VTU format after
// also associating the current time and time step number with this
// file, and providing physical units for the two fields to be
// output.
template <int dim>
void CathodeRaySimulator<dim>::output_results() const
{
{
ElectricFieldPostprocessor<dim> electric_field;
DataOut<dim> data_out;
data_out.attach_dof_handler(dof_handler);
data_out.add_data_vector(solution, "electric_potential");
data_out.add_data_vector(solution, electric_field);
data_out.build_patches();
DataOutBase::VtkFlags output_flags;
output_flags.time = time.get_current_time();
output_flags.cycle = time.get_step_number();
output_flags.physical_units["electric_potential"] = "V";
output_flags.physical_units["electric_field"] = "V/m";
data_out.set_flags(output_flags);
std::ofstream output("solution-" +
Utilities::int_to_string(time.get_step_number(), 4) +
".vtu");
data_out.write_vtu(output);
}
// Output the particle positions and properties is not more complicated. The
// Particles::DataOut class plays the role of the DataOut class for
// particles, and all we have to do is tell that class where to take
// particles from and how to interpret the `dim` components of the
// properties -- namely, as a single vector indicating the velocity, rather
// than as `dim` scalar properties. The rest is then the same as above:
{
Particles::DataOut<dim, dim> particle_out;
particle_out.build_patches(
particle_handler,
std::vector<std::string>(dim, "velocity"),
std::vector<DataComponentInterpretation::DataComponentInterpretation>(
dim, DataComponentInterpretation::component_is_part_of_vector));
DataOutBase::VtkFlags output_flags;
output_flags.time = time.get_current_time();
output_flags.cycle = time.get_step_number();
output_flags.physical_units["velocity"] = "m/s";
particle_out.set_flags(output_flags);
std::ofstream output("particles-" +
Utilities::int_to_string(time.get_step_number(), 4) +
".vtu");
particle_out.write_vtu(output);
}
}
// @sect4{CathodeRaySimulator::run}
// The last member function of the principal class of this program is then the
// driver. At the top, it refines the mesh a number of times by solving the
// problem (with not particles yet created) on a sequence of finer and finer
// meshes.
template <int dim>
void CathodeRaySimulator<dim>::run()
{
make_grid();
// do a few refinement cycles up front
const unsigned int n_pre_refinement_cycles = 3;
for (unsigned int refinement_cycle = 0;
refinement_cycle < n_pre_refinement_cycles;
++refinement_cycle)
{
setup_system();
assemble_system();
solve_field();
refine_grid();
}
// Now do the loop over time. The sequence of steps follows closely the
// outline of the algorithm discussed in the introduction. As discussed in
// great detail in the documentation of the DiscreteTime class, while we
// move the field and particle information forward by one time step, the
// time stored in the `time` variable is not consistent with where (some of)
// these quantities are (in the diction of DiscreteTime, this is the "update
// stage"). The call to `time.advance_time()` makes everything consistent
// again by setting the `time` variable to the time at which the field and
// particles already are, and once we are in this "consistent stage", we can