Chi-Tech
CodeTut93.h
Go to the documentation of this file.
1/** \page CodeTut93 Coding Tutorial 93 - Ray-tracing for uncollided flux using random rays.
2
3## Table of Contents
4- \ref CodeTut93Sec1
5 - \ref CodeTut93Sec1_1
6 - \ref CodeTut93Sec1_2
7 - \ref CodeTut93Sec1_3
8- \ref CodeTut93Sec2
9- \ref CodeTut93Sec3
10- \ref CodeTut93Sec4
11 - \ref CodeTut93Sec4_1
12 - \ref CodeTut93Sec4_2
13 - \ref CodeTut93Sec4_3
14- \ref CodeTut93Sec5
15- \ref CodeTut93Sec6
16- \ref CodeTut93Sec7
17- \ref CodeTut93Sec8
18- \ref CodeTut93Sec9
19
20\section CodeTut93Sec1 1 Introduction
21Monte Carlo based flux estimators traditionally use a number of tracks, traced
22within a volume, to estimate the scalar flux. For \f$ N_t \f$ amount of tracks
23traced inside a volume, \f$ V \f$, originating from a sample of \f$ N_p \f$
24number of particles/rays, the scalar flux, \f$ \phi \f$, can be estimated with
25\f[
26\phi \approx \frac{1}{N_p V} \sum_t^{N_t} \ell_t,
27\f]
28where \f$ \ell_t \f$ is the \f$ t \f$-th track length. This simply means that
29the scalar flux is the "average track length per unit volume".
30
31The individual tracks of these particles can be assigned a weight, \f$ w_t \f$,
32enabling a multitude of features.
33\f[
34\phi \approx \frac{1}{N_p V} \sum_t^{N_t} \ell_t w_t,
35\f]
36For our purposes we are interested in three possible
37weightings, i) weighting by a spherical harmonic, which can be used to compute
38flux moments, ii) weighting by average FE
39shape function values, allowing the projection of flux onto a FE space, and
40finally iii) weighting with an exponential attenuation, allowing the computation
41of uncollided flux.
42
43\subsection CodeTut93Sec1_1 1.1 Weighting with a spherical harmonic
44Applying a weight with a spherical harmonic is conceptually simple. For the
45\f$ t \f$-th track we simply set the weight to the spherical harmonic evaluated
46with the direction, \f$ \boldsymbol{\Omega}_t \f$, of the track,
47\f[
48\phi_{\ell m} \approx \frac{1}{N_p V} \sum_t^{N_t} \ell_t Y_{\ell m}(
49\boldsymbol{\Omega}_t),
50\f]
51
52\subsection CodeTut93Sec1_2 1.2 Weighting by average FE shape function values
53For a DFEM projection of the fluxes we require nodal values, on each cell, for
54a given flux quantity, \f$ \phi \f$. Therefore we seek \f$ \phi_h \f$ such that
55\f{eqnarray*}{
56\int_V b_i \phi_h dV = \int_V b_i \phi dV
57\f}
58and since \f$ \phi_h = \sum_j b_j \phi_j \f$ we need to solve the cell-by-cell
59system defined by
60\f{eqnarray*}{
61\sum_j \phi_j \int_V b_i b_j dV = \int_V b_i \phi dV.
62\f}
63In this system the entries \f$ \int_V b_i b_j dV \f$ are simply the entries of
64the mass matrix, \f$ M_{ij} = \int_V b_i b_j dV \f$, and we still need to find
65the rhs-entries \f$ \int_V b_i \phi dV \f$. This is where we will use the track
66length based estimators by weighting with the shape functions b_i.
67
68
69In this formulation we have
70\f{eqnarray*}{
71\int_V b_i \phi dV \approx \frac{1}{N_p} \sum_t \ell_t w_t^{i,avg}
72\f}
73where
74\f[
75w_t^{i,avg} = \frac{ \int_{s_a}^{s_b} b_i(s\mapsto \mathbf{x}) ds }
76 {\int_{s_a}^{s_b} ds} =
77\frac{ \int_{s_a}^{s_b} b_i(s\mapsto \mathbf{x}) ds }
78 {\ell_t},
79\f]
80the average basis function value along the track. For ordinary Lagrange FE shape
81functions, which are not defined piecewise, the integral in the numerator can be
82obtained exactly using a numerical quadrature. For our applications, where we
83use the PWLD FE shape functions we need to split this integral per segment of
84the basic cell crossed.
85
86For example, consider the polygon below, where a ray is traced from position
87\f$ s_a \f$ to \f$ s_b \f$.
88\image html CodingTutorials/Tut93_segments.png width=350px
89
90The track traced across the cell needs to be split into the segments defined by the
91sub-triangles of the polygon it crossed (for polyhedrons this would be the
92sub-tetrahedrons). Therefore the track \f$ s_a \to s_b \f$
93needs to be split into tracks \f$ s_0 \to s_1 \f$, \f$ s_1 \to s_2 \f$ and
94\f$ s_2 \to s_3 \f$ as per the figure. Therefore the integral becomes
95
96\f[
97\int_{s_a}^{s_b} b_i(s\mapsto \mathbf{x}) ds =
98\int_{s_0}^{s_1} b_i(s\mapsto \mathbf{x}) ds +
99\int_{s_1}^{s_2} b_i(s\mapsto \mathbf{x}) ds +
100\int_{s_2}^{s_3} b_i(s\mapsto \mathbf{x}) ds
101\f]
102which we can evaluate analytically since it is linear on each
103segment.
104
105<b>Note:</b> The mapping of \f$ s\mapsto \mathbf{x} \f$ can be quite expensive so
106in this particular case it would be better to evaluate the shape function at
107the half-way point of each segment, after which the integral becomes
108\f[
109\int_{s_a}^{s_b} b_i(s\mapsto \mathbf{x}) ds =
110b_i(\frac{s_0 + s_1}{2}\mapsto \mathbf{x}) (s_1 - s_0) +
111b_i(\frac{s_1 + s_2}{2}\mapsto \mathbf{x}) (s_2 - s_1) +
112b_i(\frac{s_2 + s_3}{2}\mapsto \mathbf{x}) (s_3 - s_2) +
113\f]
114
115\subsection CodeTut93Sec1_3 1.3 Weighting with an exponential attenuation
116Weighting with an exponential attenuation adds the final piece of weighting
117necessary to efficiently compute the uncollided flux.
118
119The exponential attenuation of the uncollided flux, \f$ \psi \f$, along the path
120of a ray with direction \f$ \boldsymbol{\Omega} \f$ is expressed as
121\f[
122\frac{d\psi}{ds} = -\sigma_t(s) \psi(s)
123\f]
124where \f$ s \f$ is the distance traveled and \f$ \sigma_t \f$ is the total
125cross section. From this model we can compute the attenuation across a cell,
126with constant \f$ \sigma_t \f$, as
127\f[
128\psi(s) = \psi(s=0) e^{-\sigma_t s}
129\f]
130where \f$ \psi(s=0) \f$ is the value of \f$ \psi \f$ when it entered the cell.
131
132
133To assimilate all of this into a raytracing algorithm we start a source particle
134with a weight, \f$ w^p = 1 \f$, which acts as the proxy for \f$ \psi \f$
135(i.e., \f$ w^p \equiv \psi \f$). From
136this we can determine the nodal uncollided flux, \f$ \phi^{uc} \f$, in a similar
137fashion as we would determine the regular flux, i.e.,
138\f[
139\sum_j \phi_j^{uc} \int_V b_i b_j dV = \int_V b_i \phi^{uc} dV,
140\f]
141however, now we need additional treatment for the integral containing
142\f$ \phi^{uc} \f$, for which we have
143\f[
144\int_V b_i \phi^{uc} dV \approx \frac{1}{N_p} \sum_t \ell_t w_t^{i,avg},
145\f]
146where, this time,
147\f[
148w_t^{i,avg} =
149\frac{ \int_{s_a}^{s_b} w^p(s) b_i(s\mapsto \mathbf{x}) ds }
150 {\ell_t}.
151\f]
152Note here that \f$ w^p \f$ is a function of position, specifically
153\f[
154w^p(s) = w^p(s=s_a) e^{-\sigma_t s}
155\f]
156and so is the basis function \f$ b_i \f$.
157
158The form of this integral needs to split into segments in the same way we
159did in the previous subsection. Therefore, given K amount of segments, we now have
160\f[
161\ell_t w_t^{i,avg} = \sum_{k=0}^{K-1} \ell_{tk} w_{tk}^{i,avg}
162\f]
163where \f$ \ell_{tk} \f$ is the track length of the \f$ k \f$-th segment and
164\f$ w_{tk}^{i,avg} \f$ is the average weight of this segment. The weight is
165computed with
166\f[
167w_{tk}^{i,avg} =
168\frac{ \int_{s_k}^{s_{k+1}} w^p(s) b_i(s\mapsto \mathbf{x}) ds }
169 {\ell_{tk}}.
170\f]
171where \f$ s_k \f$ and \f$ s_{k+1} \f$ are the beginning and ending positions of
172segment \f$ k \f$ respectively.
173
174Note here that, since we have an expression for \f$ w^p \f$, we can compute
175\f$ w^p \f$ at any point along track \f$ t \f$ including at the start of any
176segment. Therefore we define
177\f$
178w_k^p = w^p(s=s_k)
179\f$
180which allows us to express \f$ w^p \f$ as
181\f[
182w^p(s) = w_k^p e^{-\sigma_t(s-s_k)}, \quad \quad \quad s\in[s_k,s_{k+1}]
183\f]
184
185
186Additionally we express the basis functions on a segment, since we
187know the shape function is linear on the segment, as
188\f[
189b_i(s) =
190b_{i,k} \frac{s_{k+1}-s}{s_{k+1}-s_k} +
191b_{i,k+1} \frac{s-s_k}{s_{k+1}-s_k}
192\f]
193where \f$ b_{i,k} \f$ and \f$ b_{i,k+1} \f$ are the basis function values at the
194beginning and end of the segment, respectively. These two expressions allow us to
195evaluate the segment average weight as
196\f[
197w_{tk}^{i,avg} =
198\frac{1}{\ell_{tk}}
199\int_{s_k}^{s_{k+1}} w_k^p e^{-\sigma_t(s-s_k)}
200\biggr[
201b_{i,k} \frac{s_{k+1}-s}{s_{k+1}-s_k} +
202b_{i,k+1} \frac{s-s_k}{s_{k+1}-s_k}
203\biggr] ds
204\f]
205and since \f$ s_{k+1}-s_k = \ell_{tk} \f$ we can simplify this expression as
206
207\f{align*}{
208w_{tk}^{i,avg} &=
209\frac{1}{\ell_{tk}}
210\int_{0}^{\ell_{tk}} w_k^p e^{-\sigma_t s' }
211\biggr[
212b_{i,k} \frac{\ell_{tk}-s'}{\ell_{tk}} +
213b_{i,k+1} \frac{s'}{\ell_{tk}}
214\biggr] ds'
215\\
216&= \frac{1}{\ell_{tk}^2}
217\int_{0}^{\ell_{tk}} w_k^p e^{-\sigma_t s' }
218\biggr[
219b_{i,k} \ell_{tk} +
220(b_{i,k+1} - b_{i,k} ) s'
221\biggr] ds'
222\f}
223which is in the general form
224\f[
225w_{tk}^{i,avg} =
226\frac{w_k^p}{\ell_{tk}^2}
227\int_{0}^{\ell_{tk}} e^{-\sigma_t s'}
228\biggr[
229C_0 + C_1 s'
230\biggr] ds'
231\f]
232where
233\f[
234C_0 = b_{i,k} \ell_{tk}
235\f]
236\f[
237C_1 = b_{i,k+1} - b_{i,k}.
238\f]
239
240With these constants defined the expression can be evaluated analytically
241
242\f[
243w_{tk}^{i,avg} =
244\frac{w_k^p}{\ell_{tk}^2}
245\biggr[
246\frac{C_0}{\sigma_t} (1-e^{-\sigma_t \ell_{tk}})
247+
248\frac{C_1}{\sigma_t^2}
249\biggr(
250 1 - (1 + \sigma_t \ell_{tk} )
251\biggr)e^{-\sigma_t \ell_{tk}}
252\biggr]
253\f]
254
255
256\section CodeTut93Sec2 2 Program setup
257We start by getting the grid as usual:
258\code
259auto grid_ptr = chi_mesh::GetCurrentHandler().GetGrid();
260const auto& grid = *grid_ptr;
261
262chi::log.Log() << "Global num cells: " << grid.GetGlobalNumberOfCells();
263
264const int dimension = (grid.Attributes() & chi_mesh::DIMENSION_1)? 1 :
265 (grid.Attributes() & chi_mesh::DIMENSION_2)? 2 :
266 (grid.Attributes() & chi_mesh::DIMENSION_3)? 3 : 0;
267\endcode
268Note here that we grab the `dimension`.
269
270
271Next we set a number of parameters important to the simulation:
272\code
273const size_t num_groups = 1;
274const size_t scattering_order = 1;
275const auto& L = scattering_order;
276const size_t num_moments =
277 (dimension == 1)? L + 1 :
278 (dimension == 2)? (L+1)*(L+2)/2 :
279 (dimension == 3)? (L+1)*(L+1) : 0;
280const double sigma_t = 0.27;
281
282// Build harmonic map
283std::vector<std::pair<int,int>> m_to_ell_em_map;
284if (dimension == 1)
285 for (int ell=0; ell<=scattering_order; ell++)
286 m_to_ell_em_map.emplace_back(ell,0);
287else if (dimension == 2)
288 for (int ell=0; ell<=scattering_order; ell++)
289 for (int m=-ell; m<=ell; m+=2)
290 m_to_ell_em_map.emplace_back(ell,m);
291else if (dimension == 3)
292 for (int ell=0; ell<=scattering_order; ell++)
293 for (int m=-ell; m<=ell; m++)
294 m_to_ell_em_map.emplace_back(ell,m);
295\endcode
296The interesting items here includes the `scattering_order` and the map from
297linear moment index to harmonic indices, `m_to_ell_em_map`. See the
298<a href="https://github.com/doctor-janv/whitepapers/blob/main/ChiTech-LBS-TheoryManual/ChiTech-LBS-TheoryManual_v_1_13.pdf">LBS Whitepaper</a>
299for the detail of this but in a nutshell... Only some of the harmonics are relevant
300in certain dimensions.
301
302
303Next we build the spatial discretization, which in this case would be a PWLD
304discretization, and we do this as normal:
305\code
306typedef std::shared_ptr<chi_math::SpatialDiscretization> SDMPtr;
307SDMPtr sdm_ptr = chi_math::SpatialDiscretization_PWLD::New(grid_ptr);
308const auto& sdm = *sdm_ptr;
309\endcode
310
311For the unknown manager and DOF-counts we build an unknown manager as follows:
312\code
313chi_math::UnknownManager phi_uk_man;
314for (size_t m=0; m<num_moments; ++m)
315 phi_uk_man.AddUnknown(chi_math::UnknownType::VECTOR_N, num_groups);
316\endcode
317
318And then grab the dof-counts
319\code
320const size_t num_fem_local_dofs = sdm.GetNumLocalDOFs(phi_uk_man);
321const size_t num_fem_globl_dofs = sdm.GetNumGlobalDOFs(phi_uk_man);
322
323chi::log.Log() << "Num local FEM DOFs: " << num_fem_local_dofs;
324chi::log.Log() << "Num globl FEM DOFs: " << num_fem_globl_dofs;
325\endcode
326
327This allows us to define the business end, which is the flux tally vector:
328\code
329std::vector<double> phi_tally(num_fem_local_dofs, 0.0);
330\endcode
331
332\section CodeTut93Sec3 3 The particle/ray data structure
333The particle/ray data structure is the packet of data that we will be sending
334around in our ray tracing algorithm. First we need the basic structure:
335\code
336typedef chi_mesh::Vector3 Vec3;
337struct Particle
338{
339 Vec3 position = {0.0,0.0,0.0};
340 Vec3 direction = {0.0,0.0,0.0};
341 int energy_group = 0;
342 double weight = 1.0;
343
344 uint64_t cell_id = 0;
345
346 bool alive = true;
347};
348\endcode
349This is all basic stuff. Essentially the particle's state is tracked with the
350members `position`, `direction`, `energy_group` and `weight`. The other two
351members are simply auxiliary items to assist with the transport process.
352
353Next we define a source, along with the determining which cell contains the
354source:
355\code
356const Vec3 source_pos = {0.0,0.0,0.0};
357
358chi_mesh::Cell const* source_cell_ptr = nullptr;
359
360for (auto& cell : grid.local_cells)
361 if (grid.CheckPointInsideCell(cell, source_pos))
362 {
363 source_cell_ptr = &cell;
364 break;
365 }
366if (source_cell_ptr == nullptr)
367 throw std::logic_error(fname + ": Source cell not found.");
368
369const uint64_t source_cell_id = source_cell_ptr->global_id;
370\endcode
371Notice here we used the grid utility `CheckPointInsideCell`.
372
373\section CodeTut93Sec4 4 Utility lambdas
374In this section we define 3 utility functions in the form of c++ lambdas.
375i) A routine to sample a random direction. This gets used when we sample the
376source.
377ii) A routine to contribute a track to a PWLD tally. This is very complicated
378and will be explained.
379iii) A routine to approximate a given cell's size. The approximate cell sizes
380are used by the raytracer (which we will discuss later) to set appropriate
381tolerances used during the sub-routines of the raytracer.
382
383\subsection CodeTut93Sec4_1 4.1 Sampling a random direction
384Given a random number generator we can use two random numbers to sample the
385azimuthal- and polar directions. The azimuthal angle is sampled uniformly, i.e.,
386\f$ \varphi \in [0,2\pi] \f$ whilst the polar angle is determined by sampling
387the cosine of the polar angle uniformly, i.e.,
388\f$ \cos \theta = \mu \in [-1,1] \f$.
389
390\code
391chi_math::RandomNumberGenerator rng;
392auto SampleRandomDirection = [&rng]()
393{
394 double costheta = 2.0*rng.Rand() - 1.0;
395 double theta = acos(costheta);
396 double varphi = rng.Rand()*2.0*M_PI;
397
398 return chi_mesh::Vector3{sin(theta) * cos(varphi),
399 sin(theta) * sin(varphi),
400 cos(theta)};
401};
402\endcode
403It is important to note that one should only use a single random number, which,
404gets reused, and not define new ones since the random seed for new generators
405will be the same.
406
407\subsection CodeTut93Sec4_2 4.2 PWLD Tally contribution
408The tally contribution routine takes a track, defined by a starting and ending
409position, and contributes it to the specified cell's tally information. Of course
410additional information about the particle creating the track is also provided,
411i.e., the direction, energy group index and weight at the starting position.
412
413\code
414auto ContributePWLDTally = [&sdm,&grid,&phi_tally,&phi_uk_man,&sigma_t,
415 &num_moments,&m_to_ell_em_map](
416 const chi_mesh::Cell& cell,
417 const Vec3& positionA,
418 const Vec3& positionB,
419 const Vec3& omega,
420 const int g,
421 double weight)
422{
423 const auto& cell_mapping = sdm.GetCellMapping(cell);
424 const size_t num_nodes = cell_mapping.NumNodes();
425
426 const auto phi_theta = chi_math::OmegaToPhiThetaSafe(omega);
427 const double phi = phi_theta.first;
428 const double theta = phi_theta.second;
429
430 std::vector<double> segment_lengths;
431 chi_mesh::PopulateRaySegmentLengths(grid, //input
432 cell, //input
433 positionA, //input
434 positionB, //input
435 omega, //input
436 segment_lengths); //output
437
438 std::vector<double> shape_values_k; //At s_k
439 std::vector<double> shape_values_kp1; //At s_{k+1}
440
441 cell_mapping.ShapeValues(positionA, //input
442 shape_values_k); //output
443
444 double d_run_sum = 0.0;
445 for (const auto& segment_length_k : segment_lengths)
446 {
447 d_run_sum += segment_length_k;
448 const double& d = d_run_sum;
449
450 cell_mapping.ShapeValues(positionA+omega*d, shape_values_kp1);
451
452 const auto& b_ik = shape_values_k;
453 const auto& b_ikp1 = shape_values_kp1;
454 const double& ell_k = segment_length_k;
455
456 for (size_t i=0; i<num_nodes; ++i)
457 {
458 const double C0 = b_ik[i] * ell_k;
459 const double C1 = b_ikp1[i] - b_ik[i];
460
461 for (size_t m=0; m < num_moments; ++m)
462 {
463 const int64_t dof_map = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
464
465 //================= Apply harmonic weight
466 const auto& ell_em = m_to_ell_em_map.at(m);
467 const int ell = ell_em.first;
468 const int em = ell_em.second;
469
470 double w_harmonic = chi_math::Ylm(ell, em, phi, theta);
471
472 //================= Apply exponential attenuation weight
473 double w_exp = (C0 / sigma_t) * (1.0 - exp(-sigma_t * ell_k)) +
474 (C1 / (sigma_t * sigma_t)) *
475 (1.0 - (1 + sigma_t * ell_k) * exp(-sigma_t * ell_k));
476 w_exp *= weight / (ell_k * ell_k);
477
478 //================= Combine
479 double w_avg = w_harmonic * w_exp;
480
481 phi_tally[dof_map] += ell_k * w_avg ;
482 }//for moment m
483 }//for node i
484
485 shape_values_k = shape_values_kp1;
486 weight *= exp(-sigma_t * segment_length_k);
487 }//for d
488};
489\endcode
490
491The first portion of this routine is just housekeeping,
492\code
493const auto& cell_mapping = sdm.GetCellMapping(cell);
494const size_t num_nodes = cell_mapping.NumNodes();
495
496const auto phi_theta = chi_math::OmegaToPhiThetaSafe(omega);
497const double phi = phi_theta.first;
498const double theta = phi_theta.second;
499\endcode
500We get the cell mapping, number of nodes, and we convert the direction vector
501to a \f$ (\varphi, \theta) \f$ pair. The latter will be used for the harmonic
502weighting.
503
504Next we determing the segments crossed by this track. This information is
505populated into a vector of segment lengths, sorted according to the direction
506traveled by the ray, by using the routine
507`chi_mesh::PopulateRaySegmentLengths()`.
508\code
509std::vector<double> segment_lengths;
510chi_mesh::PopulateRaySegmentLengths(grid, //input
511 cell, //input
512 positionA, //input
513 positionB, //input
514 omega, //input
515 segment_lengths); //output
516\endcode
517
518We then declare two vectors that will hold the shape function values at
519different places on the segments. We can immediately determine the shape values
520at \f$ s_0 \f$ since this will be at position A.
521\code
522std::vector<double> shape_values_k; //At s_k
523std::vector<double> shape_values_kp1; //At s_{k+1}
524
525cell_mapping.ShapeValues(positionA, //input
526 shape_values_k); //output
527\endcode
528
529
530Next we start looping over the segments.
531\code
532double d_run_sum = 0.0;
533for (const auto& segment_length_k : segment_lengths)
534{
535 d_run_sum += segment_length_k;
536 const double& d = d_run_sum;
537
538 cell_mapping.ShapeValues(positionA+omega*d, shape_values_kp1);
539
540 const auto& b_ik = shape_values_k;
541 const auto& b_ikp1 = shape_values_kp1;
542 const double& ell_k = segment_length_k;
543\endcode
544We determine the end position of the segment using a running sum of the total
545segments traversed. We then populate the shape function values at \f$ s_{k+1} \f$
546and rename both sets of shape function values and the segment length for
547convenience.
548
549Next we loop over the nodes and moments.
550\code
551for (size_t i=0; i<num_nodes; ++i)
552{
553 const double C0 = b_ik[i] * ell_k;
554 const double C1 = b_ikp1[i] - b_ik[i];
555
556 for (size_t m=0; m < num_moments; ++m)
557 {
558 const int64_t dof_map = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
559\endcode
560Once a node is identified we compute the constants C0 and C1. Also once in the
561moment loop we can obtain the DOF local index of the tally.
562
563
564Next we compute the harmonic weighting:
565\code
566const auto& ell_em = m_to_ell_em_map.at(m);
567const int ell = ell_em.first;
568const int em = ell_em.second;
569
570double w_harmonic = chi_math::Ylm(ell, em, phi, theta);
571\endcode
572
573
574Then we compute the exponential weighting based on the formula
575\f[
576w_{tk}^{i,avg} =
577\frac{w_k^p}{\ell_{tk}^2}
578\biggr[
579\frac{C_0}{\sigma_t} (1-e^{-\sigma_t \ell_{tk}})
580+
581\frac{C_1}{\sigma_t^2}
582\biggr(
583 1 - (1 + \sigma_t \ell_{tk} )
584\biggr)e^{-\sigma_t \ell_{tk}}
585\biggr]
586\f]
587with the code
588\code
589double w_exp = (C0 / sigma_t) * (1.0 - exp(-sigma_t * ell_k)) +
590 (C1 / (sigma_t * sigma_t)) *
591 (1.0 - (1 + sigma_t * ell_k) * exp(-sigma_t * ell_k));
592 w_exp *= weight / (ell_k * ell_k);
593\endcode
594
595Finally, the average weight is computed and the tally contribution is made
596\code
597double w_avg = w_harmonic * w_exp;
598
599phi_tally[dof_map] += ell_k * w_avg ;
600\endcode
601
602At the end of each segment being processed we copy the shape function values
603at \f$ s_{k+1} \f$ to \f$ s_k \f$, preventing us from having to compute the values
604at \f$ s_k \f$ again (which can be expensive). We also apply the exponential
605attenuation to the particle weight over the segment.
606\code
607shape_values_k = shape_values_kp1;
608weight *= exp(-sigma_t * segment_length_k);
609\endcode
610
611\subsection CodeTut93Sec4_3 4.3 Approximating cell size
612To obtain a very rough estimate of a cell's size we simply determine its
613bounding box:
614\code
615auto GetCellApproximateSize = [&grid](const chi_mesh::Cell& cell)
616{
617 const auto& v0 = grid.vertices[cell.vertex_ids[0]];
618 double xmin = v0.x, xmax = v0.x;
619 double ymin = v0.y, ymax = v0.y;
620 double zmin = v0.z, zmax = v0.z;
621
622 for (uint64_t vid : cell.vertex_ids)
623 {
624 const auto& v = grid.vertices[vid];
625
626 xmin = std::min(xmin, v.x); xmax = std::max(xmax, v.x);
627 ymin = std::min(ymin, v.y); ymax = std::max(ymax, v.y);
628 zmin = std::min(zmin, v.z); zmax = std::max(zmax, v.z);
629 }
630
631 return (chi_mesh::Vector3(xmin, ymin, zmin) -
632 chi_mesh::Vector3(xmax, ymax, zmax)).Norm();
633};
634\endcode
635The code here should be self explanatory.
636
637\section CodeTut93Sec5 5 The raytracer
638Instantiating a `chi_mesh::RayTracer` object is very simple. It just needs the
639grid and the approximate cell sizes.
640\code
641std::vector<double> cell_sizes(grid.local_cells.size(), 0.0);
642for (const auto& cell : grid.local_cells)
643 cell_sizes[cell.local_id] = GetCellApproximateSize(cell);
644
645chi_mesh::RayTracer ray_tracer(grid, cell_sizes);
646\endcode
647
648\section CodeTut93Sec6 6 Executing the algorithms
649The basic process of simulating all the rays is fairly simple
650\code
651const size_t num_particles = 10'000'000;
652for (size_t n=0; n<num_particles; ++n)
653{
654 if (n % size_t(num_particles/10.0) == 0)
655 std::cout << "#particles = " << n << "\n";
656 //====================================== Create the particle
657 const auto omega = SampleRandomDirection();
658 Particle particle{source_pos, //position
659 omega, //direction
660 0, //e_group
661 1.0, //weight
662 source_cell_id, //cell_id
663 true}; //alive
664
665 while (particle.alive)
666 {
667 //=============================== Get the current cell
668 const auto& cell = grid.cells[particle.cell_id];
669
670 //=============================== Perform the trace
671 // to the next surface
672 auto destination_info = ray_tracer.TraceRay(cell,
673 particle.position,
674 particle.direction);
675
676 const Vec3& end_of_track_position = destination_info.pos_f;
677
678 //=============================== Make tally contribution
679 const int g = particle.energy_group;
680 if (sdm.type == PWLD)
681 ContributePWLDTally(cell,
682 particle.position, //positionA
683 end_of_track_position, //positionB
684 particle.direction, //omega
685 g, //
686 particle.weight); //weight at A
687
688 //=============================== Process cell transfer
689 // or death
690 if (not destination_info.particle_lost)
691 {
692 const auto& f = destination_info.destination_face_index;
693 const auto& current_cell_face = cell.faces[f];
694
695 if (current_cell_face.has_neighbor)
696 particle.cell_id = current_cell_face.neighbor_id;
697 else
698 particle.alive = false; //Death at the boundary
699 }
700 else
701 {
702 std::cout << "particle" << n << " lost "
703 << particle.position.PrintStr() << " "
704 << particle.direction.PrintStr() << " "
705 << "\n";
706 break;
707 }
708
709 const auto& pA = particle.position;
710 const auto& pB = end_of_track_position;
711 particle.weight *= exp(-sigma_t*(pB-pA).Norm()); //Attenuation
712 particle.position = end_of_track_position;
713 }//while ray alive
714
715}//for ray n
716\endcode
717
718We start the process with the loop
719\code
720const size_t num_particles = 10'000'000;
721for (size_t n=0; n<num_particles; ++n)
722{
723 if (n % size_t(num_particles/10.0) == 0)
724 std::cout << "#particles = " << n << "\n";
725\endcode
726The information printing line simply prints at each 10% of completion.
727
728We then create a source particle/ray
729\code
730const auto omega = SampleRandomDirection();
731Particle particle{source_pos, //position
732 omega, //direction
733 0, //e_group
734 1.0, //weight
735 source_cell_id, //cell_id
736 true}; //alive
737\endcode
738
739Next we keep transporting the particle as long as it is alive. The beginning of
740this loop is
741\code
742while (particle.alive)
743{
744 //=============================== Get the current cell
745 const auto& cell = grid.cells[particle.cell_id];
746
747 //=============================== Perform the trace
748 // to the next surface
749 auto destination_info = ray_tracer.TraceRay(cell,
750 particle.position,
751 particle.direction);
752
753 const Vec3& end_of_track_position = destination_info.pos_f;
754\endcode
755After a trace we have one single track within a cell.
756
757We then contribute the PWLD tally
758\code
759ContributePWLDTally(cell,
760 particle.position, //positionA
761 end_of_track_position, //positionB
762 particle.direction, //omega
763 particle.energy_group, //g
764 particle.weight); //weight at A
765\endcode
766
767Next we process the transfer of the particle to the next cell. If the particle
768hit a cell face without a neighbor then the particle is killed (i.e., `alive`
769set to false. Under some circumstances the raytracer could also fail, resulting
770in a lost particle, for which we print a verbose message.
771\code
772if (not destination_info.particle_lost)
773{
774 const auto& f = destination_info.destination_face_index;
775 const auto& current_cell_face = cell.faces[f];
776
777 if (current_cell_face.has_neighbor)
778 particle.cell_id = current_cell_face.neighbor_id;
779 else
780 particle.alive = false; //Death at the boundary
781}
782else
783{
784 std::cout << "particle" << n << " lost "
785 << particle.position.PrintStr() << " "
786 << particle.direction.PrintStr() << " "
787 << "\n";
788 break;
789}
790\endcode
791
792Lastly we update the particle's attenuation and position
793\code
794const auto& pA = particle.position;
795const auto& pB = end_of_track_position;
796particle.weight *= exp(-sigma_t*(pB-pA).Norm()); //Attenuation
797particle.position = end_of_track_position;
798\endcode
799
800
801
802\section CodeTut93Sec7 7 Post-processing the tallies
803The tallies up to this point are still in raw format. We need to convert them
804to the project fashion we want.
805\code
806for (const auto& cell : grid.local_cells)
807{
808 //====================================== Compute mass matrix
809 // and its inverse
810 const auto& cell_mapping = sdm.GetCellMapping(cell);
811 const auto& qp_data = cell_mapping.MakeVolumeQuadraturePointData();
812 const size_t num_nodes = cell_mapping.NumNodes();
813
814 MatDbl M(num_nodes, VecDbl(num_nodes, 0.0));
815 for (auto qp : qp_data.QuadraturePointIndices())
816 for (size_t i=0; i<num_nodes; ++i)
817 for (size_t j=0; j<num_nodes; ++j)
818 M[i][j] += qp_data.ShapeValue(i,qp) * qp_data.ShapeValue(j, qp) *
819 qp_data.JxW(qp);
820
821 auto M_inv = chi_math::Inverse(M);
822
823 //====================================== Apply projection
824 VecDbl b(num_nodes, 0.0);
825 for (size_t m=0; m<num_moments; ++m)
826 for (size_t g=0; g<num_groups; ++g)
827 {
828 for (size_t i=0; i<num_nodes; ++i)
829 {
830 const int64_t imap = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
831 b[i] = phi_tally[imap] / num_particles;
832 }
833
834 auto c = chi_math::MatMul(M_inv, b);
835
836 for (size_t i=0; i<num_nodes; ++i)
837 {
838 const int64_t imap = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
839 phi_tally[imap] = c[i];
840 }
841 }//for group g
842
843}//for cell
844\endcode
845
846The first portion of the loop is simply housekeeping again
847\code
848for (const auto& cell : grid.local_cells)
849{
850 //====================================== Compute mass matrix
851 // and its inverse
852 const auto& cell_mapping = sdm.GetCellMapping(cell);
853 const auto& qp_data = cell_mapping.MakeVolumeQuadraturePointData();
854 const size_t num_nodes = cell_mapping.NumNodes();
855
856 MatDbl M(num_nodes, VecDbl(num_nodes, 0.0));
857 for (auto qp : qp_data.QuadraturePointIndices())
858 for (size_t i=0; i<num_nodes; ++i)
859 for (size_t j=0; j<num_nodes; ++j)
860 M[i][j] += qp_data.ShapeValue(i,qp) * qp_data.ShapeValue(j, qp) *
861 qp_data.JxW(qp);
862
863 auto M_inv = chi_math::Inverse(M);
864\endcode
865We get the cell mapping, quadrature point data, and we build the mass matrix.
866Recall that we need \f$ \phi_j^{uc} \f$ such that
867\f[
868\sum_j \phi_j^{uc} \int_V b_i b_j dV = \int_V b_i \phi^{uc} dV,
869\f]
870which requires us to solve the small system
871\f[
872M \boldsymbol{\phi}^{uc} = \mathbf{T}
873\f]
874where \f$ M_{ij} = \int_V b_i b_j dV \f$ and
875\f$ T_i = \int_V b_i \phi^{uc} dV \f$. Since the mass matrix is such a small
876matrix we just directly invert it to be used for all groups and moments.
877
878Next we loop over all moments and groups.
879\code
880VecDbl T(num_nodes, 0.0);
881for (size_t m=0; m<num_moments; ++m)
882 for (size_t g=0; g<num_groups; ++g)
883 {
884 for (size_t i=0; i<num_nodes; ++i)
885 {
886 const int64_t imap = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
887 T[i] = phi_tally[imap] / num_particles;
888 }
889
890 auto phi_uc = chi_math::MatMul(M_inv, T);
891
892 for (size_t i=0; i<num_nodes; ++i)
893 {
894 const int64_t imap = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
895 phi_tally[imap] = phi_uc[i];
896 }
897 }//for group g
898\endcode
899
900Within the moment and group loop we first set the entries of \f$ T \f$ to the
901normalized tally values. Thereafter we multiply \f$ T \f$ from the left with
902\f$ M^{-1} \f$ to get \f$ \boldsymbol{\phi}^{uc} \f$. Finally we reuse the tally
903data and set the nodal values of the tally to the uncollided projected flux.
904
905\section CodeTut93Sec8 8 Exporting field functions
906Creating the field functions is similar to what we did in previous tutorials
907\code
908//============================================= Create Field Functions
909std::vector<std::shared_ptr<chi_physics::FieldFunction>> ff_list;
910
911ff_list.push_back(std::make_shared<chi_physics::FieldFunction>(
912 "Phi", //Text name
913 sdm_ptr, //Spatial Discr.
914 chi_math::Unknown(chi_math::UnknownType::VECTOR_N,num_groups) //Unknown
915));
916
917//============================================= Localize zeroth moment
918//This routine extracts a single moment vector
919//from the vector that contains multiple moments
920const chi_math::UnknownManager m0_uk_man(
921 {chi_math::Unknown(chi_math::UnknownType::VECTOR_N,num_groups)});
922const size_t num_m0_dofs = sdm.GetNumLocalDOFs(m0_uk_man);
923
924std::vector<double> m0_phi(num_m0_dofs, 0.0);
925
926sdm.CopyVectorWithUnknownScope(phi_tally, //from vector
927 m0_phi, //to vector
928 phi_uk_man, //from dof-structure
929 0, //from unknown-id
930 m0_uk_man, //to dof-structure
931 0); //to unknown-id
932
933ff_list[0]->UpdateFieldVector(m0_phi);
934
935
936//============================================= Update field function
937chi_physics::FieldFunction::FFList const_ff_list;
938for (const auto& ff_ptr : ff_list)
939 const_ff_list.push_back(ff_ptr);
940chi_physics::FieldFunction::ExportMultipleToVTK(fname,
941 const_ff_list);
942\endcode
943
944The visualization below shows a logarithmic scale warp of the flux values for
945both the uncollided algorithm and a LBS simulation using a product quadrature
946with 96 azimuthal angles and 48 polar angles per octant (18,432 directions total).
947
948The left of the figure is the uncollided algorithm and the right is LBS. Notice
949the stochastic "noise" from the uncollided algorithm.
950
951\image html CodingTutorials/SimTest93.png width=800px
952
953\section CodeTut93Sec9 The complete program
954\code
955#include "chi_lua.h"
956
957#include "mesh/MeshHandler/chi_meshhandler.h"
958#include "mesh/MeshContinuum/chi_meshcontinuum.h"
959#include "mesh/Raytrace/raytracing.h"
960
961#include "math/SpatialDiscretization/FiniteElement/PiecewiseLinear/pwl.h"
962#include "math/RandomNumberGeneration/random_number_generator.h"
963#include "math/Quadratures/LegendrePoly/legendrepoly.h"
964
965#include "physics/FieldFunction/fieldfunction2.h"
966
967#include "chi_runtime.h"
968#include "chi_log.h"
969
970namespace chi_unit_sim_tests
971{
972
973int chiSimTest93_RayTracing(lua_State* Lstate)
974{
975 const std::string fname = "chiSimTest93_RayTracing";
976 chi::log.Log() << "chiSimTest93_RayTracing";
977
978 //============================================= Get grid
979 auto grid_ptr = chi_mesh::GetCurrentHandler().GetGrid();
980 const auto& grid = *grid_ptr;
981
982 chi::log.Log() << "Global num cells: " << grid.GetGlobalNumberOfCells();
983
984 const int dimension = (grid.Attributes() & chi_mesh::DIMENSION_1)? 1 :
985 (grid.Attributes() & chi_mesh::DIMENSION_2)? 2 :
986 (grid.Attributes() & chi_mesh::DIMENSION_3)? 3 : 0;
987
988 //============================================= Set parameters
989 const size_t num_groups = 1;
990 const size_t scattering_order = 1;
991 const auto& L = scattering_order;
992 const size_t num_moments =
993 (dimension == 1)? L + 1 :
994 (dimension == 2)? (L+1)*(L+2)/2 :
995 (dimension == 3)? (L+1)*(L+1) : 0;
996 const double sigma_t = 0.27;
997
998 // Build harmonic map
999 std::vector<std::pair<int,int>> m_to_ell_em_map;
1000 if (dimension == 1)
1001 for (int ell=0; ell<=scattering_order; ell++)
1002 m_to_ell_em_map.emplace_back(ell,0);
1003 else if (dimension == 2)
1004 for (int ell=0; ell<=scattering_order; ell++)
1005 for (int m=-ell; m<=ell; m+=2)
1006 m_to_ell_em_map.emplace_back(ell,m);
1007 else if (dimension == 3)
1008 for (int ell=0; ell<=scattering_order; ell++)
1009 for (int m=-ell; m<=ell; m++)
1010 m_to_ell_em_map.emplace_back(ell,m);
1011
1012 //============================================= Make SDM
1013 typedef std::shared_ptr<chi_math::SpatialDiscretization> SDMPtr;
1014 SDMPtr sdm_ptr = chi_math::SpatialDiscretization_PWLD::New(grid_ptr);
1015 const auto& sdm = *sdm_ptr;
1016
1017 chi_math::UnknownManager phi_uk_man;
1018 for (size_t m=0; m<num_moments; ++m)
1019 phi_uk_man.AddUnknown(chi_math::UnknownType::VECTOR_N, num_groups);
1020
1021 const size_t num_fem_local_dofs = sdm.GetNumLocalDOFs(phi_uk_man);
1022 const size_t num_fem_globl_dofs = sdm.GetNumGlobalDOFs(phi_uk_man);
1023
1024 chi::log.Log() << "Num local FEM DOFs: " << num_fem_local_dofs;
1025 chi::log.Log() << "Num globl FEM DOFs: " << num_fem_globl_dofs;
1026
1027 //============================================= Define tallies
1028 std::vector<double> phi_tally(num_fem_local_dofs, 0.0);
1029
1030 //============================================= Define particle
1031 // data structure
1032 typedef chi_mesh::Vector3 Vec3;
1033 struct Particle
1034 {
1035 Vec3 position = {0.0,0.0,0.0};
1036 Vec3 direction = {0.0,0.0,0.0};
1037 int energy_group = 0;
1038 double weight = 1.0;
1039
1040 uint64_t cell_id = 0;
1041
1042 bool alive = true;
1043 };
1044
1045 //============================================= Define source position
1046 // and find cell containing it
1047 const Vec3 source_pos = {0.0,0.0,0.0};
1048
1049 chi_mesh::Cell const* source_cell_ptr = nullptr;
1050
1051 for (auto& cell : grid.local_cells)
1052 if (grid.CheckPointInsideCell(cell, source_pos))
1053 {
1054 source_cell_ptr = &cell;
1055 break;
1056 }
1057 if (source_cell_ptr == nullptr)
1058 throw std::logic_error(fname + ": Source cell not found.");
1059
1060 const uint64_t source_cell_id = source_cell_ptr->global_id;
1061
1062 //============================================= Define lambdas
1063 chi_math::RandomNumberGenerator rng;
1064 auto SampleRandomDirection = [&rng]()
1065 {
1066 double costheta = 2.0*rng.Rand() - 1.0;
1067 double theta = acos(costheta);
1068 double varphi = rng.Rand()*2.0*M_PI;
1069
1070 return chi_mesh::Vector3{sin(theta) * cos(varphi),
1071 sin(theta) * sin(varphi),
1072 cos(theta)};
1073 };
1074
1075
1076 auto ContributePWLDTally = [&sdm,&grid,&phi_tally,&phi_uk_man,&sigma_t,
1077 &num_moments,&m_to_ell_em_map](
1078 const chi_mesh::Cell& cell,
1079 const Vec3& positionA,
1080 const Vec3& positionB,
1081 const Vec3& omega,
1082 const int g,
1083 double weight)
1084 {
1085 const auto& cell_mapping = sdm.GetCellMapping(cell);
1086 const size_t num_nodes = cell_mapping.NumNodes();
1087
1088 const auto phi_theta = chi_math::OmegaToPhiThetaSafe(omega);
1089 const double phi = phi_theta.first;
1090 const double theta = phi_theta.second;
1091
1092 std::vector<double> segment_lengths;
1093 chi_mesh::PopulateRaySegmentLengths(grid, //input
1094 cell, //input
1095 positionA, //input
1096 positionB, //input
1097 omega, //input
1098 segment_lengths); //output
1099
1100 std::vector<double> shape_values_k; //At s_k
1101 std::vector<double> shape_values_kp1; //At s_{k+1}
1102
1103 cell_mapping.ShapeValues(positionA, //input
1104 shape_values_k); //output
1105
1106 double d_run_sum = 0.0;
1107 for (const auto& segment_length_k : segment_lengths)
1108 {
1109 d_run_sum += segment_length_k;
1110 const double& d = d_run_sum;
1111
1112 cell_mapping.ShapeValues(positionA+omega*d, shape_values_kp1);
1113
1114 const auto& b_ik = shape_values_k;
1115 const auto& b_ikp1 = shape_values_kp1;
1116 const double& ell_k = segment_length_k;
1117
1118 for (size_t i=0; i<num_nodes; ++i)
1119 {
1120 const double C0 = b_ik[i] * ell_k;
1121 const double C1 = b_ikp1[i] - b_ik[i];
1122
1123 for (size_t m=0; m < num_moments; ++m)
1124 {
1125 const int64_t dof_map = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
1126
1127 //================= Apply harmonic weight
1128 const auto& ell_em = m_to_ell_em_map.at(m);
1129 const int ell = ell_em.first;
1130 const int em = ell_em.second;
1131
1132 double w_harmonic = chi_math::Ylm(ell, em, phi, theta);
1133
1134 //================= Apply exponential attenuation weight
1135 double w_exp = (C0 / sigma_t) * (1.0 - exp(-sigma_t * ell_k)) +
1136 (C1 / (sigma_t * sigma_t)) *
1137 (1.0 - (1 + sigma_t * ell_k) * exp(-sigma_t * ell_k));
1138 w_exp *= weight / (ell_k * ell_k);
1139
1140 //================= Combine
1141 double w_avg = w_harmonic * w_exp;
1142
1143 phi_tally[dof_map] += ell_k * w_avg ;
1144 }//for moment m
1145 }//for node i
1146
1147 shape_values_k = shape_values_kp1;
1148 weight *= exp(-sigma_t * segment_length_k);
1149 }//for d
1150 };
1151
1152 auto GetCellApproximateSize = [&grid](const chi_mesh::Cell& cell)
1153 {
1154 const auto& v0 = grid.vertices[cell.vertex_ids[0]];
1155 double xmin = v0.x, xmax = v0.x;
1156 double ymin = v0.y, ymax = v0.y;
1157 double zmin = v0.z, zmax = v0.z;
1158
1159 for (uint64_t vid : cell.vertex_ids)
1160 {
1161 const auto& v = grid.vertices[vid];
1162
1163 xmin = std::min(xmin, v.x); xmax = std::max(xmax, v.x);
1164 ymin = std::min(ymin, v.y); ymax = std::max(ymax, v.y);
1165 zmin = std::min(zmin, v.z); zmax = std::max(zmax, v.z);
1166 }
1167
1168 return (chi_mesh::Vector3(xmin, ymin, zmin) -
1169 chi_mesh::Vector3(xmax, ymax, zmax)).Norm();
1170 };
1171
1172 //============================================= Create raytracer
1173 std::vector<double> cell_sizes(grid.local_cells.size(), 0.0);
1174 for (const auto& cell : grid.local_cells)
1175 cell_sizes[cell.local_id] = GetCellApproximateSize(cell);
1176
1177 chi_mesh::RayTracer ray_tracer(grid, cell_sizes);
1178
1179 //============================================= Run rays
1180 const size_t num_particles = 1'000'000;
1181 for (size_t n=0; n<num_particles; ++n)
1182 {
1183 if (n % size_t(num_particles/10.0) == 0)
1184 std::cout << "#particles = " << n << "\n";
1185 //====================================== Create the particle
1186 const auto omega = SampleRandomDirection();
1187 Particle particle{source_pos, //position
1188 omega, //direction
1189 0, //e_group
1190 1.0, //weight
1191 source_cell_id, //cell_id
1192 true}; //alive
1193
1194 while (particle.alive)
1195 {
1196 //=============================== Get the current cell
1197 const auto& cell = grid.cells[particle.cell_id];
1198
1199 //=============================== Perform the trace
1200 // to the next surface
1201 auto destination_info = ray_tracer.TraceRay(cell,
1202 particle.position,
1203 particle.direction);
1204
1205 const Vec3& end_of_track_position = destination_info.pos_f;
1206
1207 //=============================== Make tally contribution
1208 ContributePWLDTally(cell,
1209 particle.position, //positionA
1210 end_of_track_position, //positionB
1211 particle.direction, //omega
1212 particle.energy_group, //g
1213 particle.weight); //weight at A
1214
1215 //=============================== Process cell transfer
1216 // or death
1217 if (not destination_info.particle_lost)
1218 {
1219 const auto& f = destination_info.destination_face_index;
1220 const auto& current_cell_face = cell.faces[f];
1221
1222 if (current_cell_face.has_neighbor)
1223 particle.cell_id = current_cell_face.neighbor_id;
1224 else
1225 particle.alive = false; //Death at the boundary
1226 }
1227 else
1228 {
1229 std::cout << "particle" << n << " lost "
1230 << particle.position.PrintStr() << " "
1231 << particle.direction.PrintStr() << " "
1232 << "\n";
1233 break;
1234 }
1235
1236 const auto& pA = particle.position;
1237 const auto& pB = end_of_track_position;
1238 particle.weight *= exp(-sigma_t*(pB-pA).Norm()); //Attenuation
1239 particle.position = end_of_track_position;
1240 }//while ray alive
1241
1242 }//for ray n
1243
1244 //============================================= Post process tallies
1245 for (const auto& cell : grid.local_cells)
1246 {
1247 //====================================== Compute mass matrix
1248 // and its inverse
1249 const auto& cell_mapping = sdm.GetCellMapping(cell);
1250 const auto& qp_data = cell_mapping.MakeVolumeQuadraturePointData();
1251 const size_t num_nodes = cell_mapping.NumNodes();
1252
1253 MatDbl M(num_nodes, VecDbl(num_nodes, 0.0));
1254 for (auto qp : qp_data.QuadraturePointIndices())
1255 for (size_t i=0; i<num_nodes; ++i)
1256 for (size_t j=0; j<num_nodes; ++j)
1257 M[i][j] += qp_data.ShapeValue(i,qp) * qp_data.ShapeValue(j, qp) *
1258 qp_data.JxW(qp);
1259
1260 auto M_inv = chi_math::Inverse(M);
1261
1262 //====================================== Apply projection
1263 VecDbl T(num_nodes, 0.0);
1264 for (size_t m=0; m<num_moments; ++m)
1265 for (size_t g=0; g<num_groups; ++g)
1266 {
1267 for (size_t i=0; i<num_nodes; ++i)
1268 {
1269 const int64_t imap = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
1270 T[i] = phi_tally[imap] / num_particles;
1271 }
1272
1273 auto phi_uc = chi_math::MatMul(M_inv, T);
1274
1275 for (size_t i=0; i<num_nodes; ++i)
1276 {
1277 const int64_t imap = sdm.MapDOFLocal(cell, i, phi_uk_man, m, g);
1278 phi_tally[imap] = phi_uc[i];
1279 }
1280 }//for group g
1281
1282 }//for cell
1283
1284 //============================================= Create Field Functions
1285 std::vector<std::shared_ptr<chi_physics::FieldFunction>> ff_list;
1286
1287 ff_list.push_back(std::make_shared<chi_physics::FieldFunction>(
1288 "Phi", //Text name
1289 sdm_ptr, //Spatial Discr.
1290 chi_math::Unknown(chi_math::UnknownType::VECTOR_N,num_groups) //Unknown
1291 ));
1292
1293 //============================================= Localize zeroth moment
1294 //This routine extracts a single moment vector
1295 //from the vector that contains multiple moments
1296 const chi_math::UnknownManager m0_uk_man(
1297 {chi_math::Unknown(chi_math::UnknownType::VECTOR_N,num_groups)});
1298 const size_t num_m0_dofs = sdm.GetNumLocalDOFs(m0_uk_man);
1299
1300 std::vector<double> m0_phi(num_m0_dofs, 0.0);
1301
1302 sdm.CopyVectorWithUnknownScope(phi_tally, //from vector
1303 m0_phi, //to vector
1304 phi_uk_man, //from dof-structure
1305 0, //from unknown-id
1306 m0_uk_man, //to dof-structure
1307 0); //to unknown-id
1308
1309 ff_list[0]->UpdateFieldVector(m0_phi);
1310
1311
1312 //============================================= Update field function
1313 chi_physics::FieldFunction::FFList const_ff_list;
1314 for (const auto& ff_ptr : ff_list)
1315 const_ff_list.push_back(ff_ptr);
1316 chi_physics::FieldFunction::ExportMultipleToVTK(fname,
1317 const_ff_list);
1318
1319 return 0;
1320}
1321
1322}//namespace chi_unit_sim_tests
1323\endcode
1324*/