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
21
Monte Carlo based flux estimators traditionally use a number of tracks, traced
22
within a volume, to estimate the scalar flux. For \f$ N_t \f$ amount of tracks
23
traced inside a volume, \f$ V \f$, originating from a sample of \f$ N_p \f$
24
number 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]
28
where \f$ \ell_t \f$ is the \f$ t \f$-th track length. This simply means that
29
the scalar flux is the "average track length per unit volume".
30
31
The individual tracks of these particles can be assigned a weight, \f$ w_t \f$,
32
enabling a multitude of features.
33
\f[
34
\phi \approx \frac{1}{N_p V} \sum_t^{N_t} \ell_t w_t,
35
\f]
36
For our purposes we are interested in three possible
37
weightings, i) weighting by a spherical harmonic, which can be used to compute
38
flux moments, ii) weighting by average FE
39
shape function values, allowing the projection of flux onto a FE space, and
40
finally iii) weighting with an exponential attenuation, allowing the computation
41
of uncollided flux.
42
43
\subsection CodeTut93Sec1_1 1.1 Weighting with a spherical harmonic
44
Applying 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
46
with 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
53
For a DFEM projection of the fluxes we require nodal values, on each cell, for
54
a 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}
58
and since \f$ \phi_h = \sum_j b_j \phi_j \f$ we need to solve the cell-by-cell
59
system 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}
63
In this system the entries \f$ \int_V b_i b_j dV \f$ are simply the entries of
64
the mass matrix, \f$ M_{ij} = \int_V b_i b_j dV \f$, and we still need to find
65
the rhs-entries \f$ \int_V b_i \phi dV \f$. This is where we will use the track
66
length based estimators by weighting with the shape functions b_i.
67
68
69
In 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}
73
where
74
\f[
75
w_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]
80
the average basis function value along the track. For ordinary Lagrange FE shape
81
functions, which are not defined piecewise, the integral in the numerator can be
82
obtained exactly using a numerical quadrature. For our applications, where we
83
use the PWLD FE shape functions we need to split this integral per segment of
84
the basic cell crossed.
85
86
For 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
90
The track traced across the cell needs to be split into the segments defined by the
91
sub-triangles of the polygon it crossed (for polyhedrons this would be the
92
sub-tetrahedrons). Therefore the track \f$ s_a \to s_b \f$
93
needs 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]
102
which we can evaluate analytically since it is linear on each
103
segment.
104
105
<b>Note:</b> The mapping of \f$ s\mapsto \mathbf{x} \f$ can be quite expensive so
106
in this particular case it would be better to evaluate the shape function at
107
the 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 =
110
b_i(\frac{s_0 + s_1}{2}\mapsto \mathbf{x}) (s_1 - s_0) +
111
b_i(\frac{s_1 + s_2}{2}\mapsto \mathbf{x}) (s_2 - s_1) +
112
b_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
116
Weighting with an exponential attenuation adds the final piece of weighting
117
necessary to efficiently compute the uncollided flux.
118
119
The exponential attenuation of the uncollided flux, \f$ \psi \f$, along the path
120
of 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]
124
where \f$ s \f$ is the distance traveled and \f$ \sigma_t \f$ is the total
125
cross section. From this model we can compute the attenuation across a cell,
126
with constant \f$ \sigma_t \f$, as
127
\f[
128
\psi(s) = \psi(s=0) e^{-\sigma_t s}
129
\f]
130
where \f$ \psi(s=0) \f$ is the value of \f$ \psi \f$ when it entered the cell.
131
132
133
To assimilate all of this into a raytracing algorithm we start a source particle
134
with 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
136
this we can determine the nodal uncollided flux, \f$ \phi^{uc} \f$, in a similar
137
fashion 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]
141
however, 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]
146
where, this time,
147
\f[
148
w_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]
152
Note here that \f$ w^p \f$ is a function of position, specifically
153
\f[
154
w^p(s) = w^p(s=s_a) e^{-\sigma_t s}
155
\f]
156
and so is the basis function \f$ b_i \f$.
157
158
The form of this integral needs to split into segments in the same way we
159
did 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]
163
where \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
165
computed with
166
\f[
167
w_{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]
171
where \f$ s_k \f$ and \f$ s_{k+1} \f$ are the beginning and ending positions of
172
segment \f$ k \f$ respectively.
173
174
Note 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
176
segment. Therefore we define
177
\f$
178
w_k^p = w^p(s=s_k)
179
\f$
180
which allows us to express \f$ w^p \f$ as
181
\f[
182
w^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
186
Additionally we express the basis functions on a segment, since we
187
know the shape function is linear on the segment, as
188
\f[
189
b_i(s) =
190
b_{i,k} \frac{s_{k+1}-s}{s_{k+1}-s_k} +
191
b_{i,k+1} \frac{s-s_k}{s_{k+1}-s_k}
192
\f]
193
where \f$ b_{i,k} \f$ and \f$ b_{i,k+1} \f$ are the basis function values at the
194
beginning and end of the segment, respectively. These two expressions allow us to
195
evaluate the segment average weight as
196
\f[
197
w_{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[
201
b_{i,k} \frac{s_{k+1}-s}{s_{k+1}-s_k} +
202
b_{i,k+1} \frac{s-s_k}{s_{k+1}-s_k}
203
\biggr] ds
204
\f]
205
and since \f$ s_{k+1}-s_k = \ell_{tk} \f$ we can simplify this expression as
206
207
\f{align*}{
208
w_{tk}^{i,avg} &=
209
\frac{1}{\ell_{tk}}
210
\int_{0}^{\ell_{tk}} w_k^p e^{-\sigma_t s' }
211
\biggr[
212
b_{i,k} \frac{\ell_{tk}-s'}{\ell_{tk}} +
213
b_{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[
219
b_{i,k} \ell_{tk} +
220
(b_{i,k+1} - b_{i,k} ) s'
221
\biggr] ds'
222
\f}
223
which is in the general form
224
\f[
225
w_{tk}^{i,avg} =
226
\frac{w_k^p}{\ell_{tk}^2}
227
\int_{0}^{\ell_{tk}} e^{-\sigma_t s'}
228
\biggr[
229
C_0 + C_1 s'
230
\biggr] ds'
231
\f]
232
where
233
\f[
234
C_0 = b_{i,k} \ell_{tk}
235
\f]
236
\f[
237
C_1 = b_{i,k+1} - b_{i,k}.
238
\f]
239
240
With these constants defined the expression can be evaluated analytically
241
242
\f[
243
w_{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
257
We start by getting the grid as usual:
258
\code
259
auto grid_ptr = chi_mesh::GetCurrentHandler().GetGrid();
260
const auto& grid = *grid_ptr;
261
262
chi::log.Log() << "Global num cells: " << grid.GetGlobalNumberOfCells();
263
264
const 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
268
Note here that we grab the `dimension`.
269
270
271
Next we set a number of parameters important to the simulation:
272
\code
273
const size_t num_groups = 1;
274
const size_t scattering_order = 1;
275
const auto& L = scattering_order;
276
const 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;
280
const double sigma_t = 0.27;
281
282
// Build harmonic map
283
std::vector<std::pair<int,int>> m_to_ell_em_map;
284
if (dimension == 1)
285
for (int ell=0; ell<=scattering_order; ell++)
286
m_to_ell_em_map.emplace_back(ell,0);
287
else 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);
291
else 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
296
The interesting items here includes the `scattering_order` and the map from
297
linear 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>
299
for the detail of this but in a nutshell... Only some of the harmonics are relevant
300
in certain dimensions.
301
302
303
Next we build the spatial discretization, which in this case would be a PWLD
304
discretization, and we do this as normal:
305
\code
306
typedef std::shared_ptr<chi_math::SpatialDiscretization> SDMPtr;
307
SDMPtr sdm_ptr = chi_math::SpatialDiscretization_PWLD::New(grid_ptr);
308
const auto& sdm = *sdm_ptr;
309
\endcode
310
311
For the unknown manager and DOF-counts we build an unknown manager as follows:
312
\code
313
chi_math::UnknownManager phi_uk_man;
314
for (size_t m=0; m<num_moments; ++m)
315
phi_uk_man.AddUnknown(chi_math::UnknownType::VECTOR_N, num_groups);
316
\endcode
317
318
And then grab the dof-counts
319
\code
320
const size_t num_fem_local_dofs = sdm.GetNumLocalDOFs(phi_uk_man);
321
const size_t num_fem_globl_dofs = sdm.GetNumGlobalDOFs(phi_uk_man);
322
323
chi::log.Log() << "Num local FEM DOFs: " << num_fem_local_dofs;
324
chi::log.Log() << "Num globl FEM DOFs: " << num_fem_globl_dofs;
325
\endcode
326
327
This allows us to define the business end, which is the flux tally vector:
328
\code
329
std::vector<double> phi_tally(num_fem_local_dofs, 0.0);
330
\endcode
331
332
\section CodeTut93Sec3 3 The particle/ray data structure
333
The particle/ray data structure is the packet of data that we will be sending
334
around in our ray tracing algorithm. First we need the basic structure:
335
\code
336
typedef chi_mesh::Vector3 Vec3;
337
struct 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
349
This is all basic stuff. Essentially the particle's state is tracked with the
350
members `position`, `direction`, `energy_group` and `weight`. The other two
351
members are simply auxiliary items to assist with the transport process.
352
353
Next we define a source, along with the determining which cell contains the
354
source:
355
\code
356
const Vec3 source_pos = {0.0,0.0,0.0};
357
358
chi_mesh::Cell const* source_cell_ptr = nullptr;
359
360
for (auto& cell : grid.local_cells)
361
if (grid.CheckPointInsideCell(cell, source_pos))
362
{
363
source_cell_ptr = &cell;
364
break;
365
}
366
if (source_cell_ptr == nullptr)
367
throw std::logic_error(fname + ": Source cell not found.");
368
369
const uint64_t source_cell_id = source_cell_ptr->global_id;
370
\endcode
371
Notice here we used the grid utility `CheckPointInsideCell`.
372
373
\section CodeTut93Sec4 4 Utility lambdas
374
In this section we define 3 utility functions in the form of c++ lambdas.
375
i) A routine to sample a random direction. This gets used when we sample the
376
source.
377
ii) A routine to contribute a track to a PWLD tally. This is very complicated
378
and will be explained.
379
iii) A routine to approximate a given cell's size. The approximate cell sizes
380
are used by the raytracer (which we will discuss later) to set appropriate
381
tolerances used during the sub-routines of the raytracer.
382
383
\subsection CodeTut93Sec4_1 4.1 Sampling a random direction
384
Given a random number generator we can use two random numbers to sample the
385
azimuthal- 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
387
the cosine of the polar angle uniformly, i.e.,
388
\f$ \cos \theta = \mu \in [-1,1] \f$.
389
390
\code
391
chi_math::RandomNumberGenerator rng;
392
auto 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
403
It is important to note that one should only use a single random number, which,
404
gets reused, and not define new ones since the random seed for new generators
405
will be the same.
406
407
\subsection CodeTut93Sec4_2 4.2 PWLD Tally contribution
408
The tally contribution routine takes a track, defined by a starting and ending
409
position, and contributes it to the specified cell's tally information. Of course
410
additional information about the particle creating the track is also provided,
411
i.e., the direction, energy group index and weight at the starting position.
412
413
\code
414
auto 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
491
The first portion of this routine is just housekeeping,
492
\code
493
const auto& cell_mapping = sdm.GetCellMapping(cell);
494
const size_t num_nodes = cell_mapping.NumNodes();
495
496
const auto phi_theta = chi_math::OmegaToPhiThetaSafe(omega);
497
const double phi = phi_theta.first;
498
const double theta = phi_theta.second;
499
\endcode
500
We get the cell mapping, number of nodes, and we convert the direction vector
501
to a \f$ (\varphi, \theta) \f$ pair. The latter will be used for the harmonic
502
weighting.
503
504
Next we determing the segments crossed by this track. This information is
505
populated into a vector of segment lengths, sorted according to the direction
506
traveled by the ray, by using the routine
507
`chi_mesh::PopulateRaySegmentLengths()`.
508
\code
509
std::vector<double> segment_lengths;
510
chi_mesh::PopulateRaySegmentLengths(grid, //input
511
cell, //input
512
positionA, //input
513
positionB, //input
514
omega, //input
515
segment_lengths); //output
516
\endcode
517
518
We then declare two vectors that will hold the shape function values at
519
different places on the segments. We can immediately determine the shape values
520
at \f$ s_0 \f$ since this will be at position A.
521
\code
522
std::vector<double> shape_values_k; //At s_k
523
std::vector<double> shape_values_kp1; //At s_{k+1}
524
525
cell_mapping.ShapeValues(positionA, //input
526
shape_values_k); //output
527
\endcode
528
529
530
Next we start looping over the segments.
531
\code
532
double d_run_sum = 0.0;
533
for (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
544
We determine the end position of the segment using a running sum of the total
545
segments traversed. We then populate the shape function values at \f$ s_{k+1} \f$
546
and rename both sets of shape function values and the segment length for
547
convenience.
548
549
Next we loop over the nodes and moments.
550
\code
551
for (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
560
Once a node is identified we compute the constants C0 and C1. Also once in the
561
moment loop we can obtain the DOF local index of the tally.
562
563
564
Next we compute the harmonic weighting:
565
\code
566
const auto& ell_em = m_to_ell_em_map.at(m);
567
const int ell = ell_em.first;
568
const int em = ell_em.second;
569
570
double w_harmonic = chi_math::Ylm(ell, em, phi, theta);
571
\endcode
572
573
574
Then we compute the exponential weighting based on the formula
575
\f[
576
w_{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]
587
with the code
588
\code
589
double 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
595
Finally, the average weight is computed and the tally contribution is made
596
\code
597
double w_avg = w_harmonic * w_exp;
598
599
phi_tally[dof_map] += ell_k * w_avg ;
600
\endcode
601
602
At the end of each segment being processed we copy the shape function values
603
at \f$ s_{k+1} \f$ to \f$ s_k \f$, preventing us from having to compute the values
604
at \f$ s_k \f$ again (which can be expensive). We also apply the exponential
605
attenuation to the particle weight over the segment.
606
\code
607
shape_values_k = shape_values_kp1;
608
weight *= exp(-sigma_t * segment_length_k);
609
\endcode
610
611
\subsection CodeTut93Sec4_3 4.3 Approximating cell size
612
To obtain a very rough estimate of a cell's size we simply determine its
613
bounding box:
614
\code
615
auto 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
635
The code here should be self explanatory.
636
637
\section CodeTut93Sec5 5 The raytracer
638
Instantiating a `chi_mesh::RayTracer` object is very simple. It just needs the
639
grid and the approximate cell sizes.
640
\code
641
std::vector<double> cell_sizes(grid.local_cells.size(), 0.0);
642
for (const auto& cell : grid.local_cells)
643
cell_sizes[cell.local_id] = GetCellApproximateSize(cell);
644
645
chi_mesh::RayTracer ray_tracer(grid, cell_sizes);
646
\endcode
647
648
\section CodeTut93Sec6 6 Executing the algorithms
649
The basic process of simulating all the rays is fairly simple
650
\code
651
const size_t num_particles = 10'000'000;
652
for (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
718
We start the process with the loop
719
\code
720
const size_t num_particles = 10'000'000;
721
for (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
726
The information printing line simply prints at each 10% of completion.
727
728
We then create a source particle/ray
729
\code
730
const auto omega = SampleRandomDirection();
731
Particle 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
739
Next we keep transporting the particle as long as it is alive. The beginning of
740
this loop is
741
\code
742
while (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
755
After a trace we have one single track within a cell.
756
757
We then contribute the PWLD tally
758
\code
759
ContributePWLDTally(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
767
Next we process the transfer of the particle to the next cell. If the particle
768
hit a cell face without a neighbor then the particle is killed (i.e., `alive`
769
set to false. Under some circumstances the raytracer could also fail, resulting
770
in a lost particle, for which we print a verbose message.
771
\code
772
if (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
}
782
else
783
{
784
std::cout << "particle" << n << " lost "
785
<< particle.position.PrintStr() << " "
786
<< particle.direction.PrintStr() << " "
787
<< "\n";
788
break;
789
}
790
\endcode
791
792
Lastly we update the particle's attenuation and position
793
\code
794
const auto& pA = particle.position;
795
const auto& pB = end_of_track_position;
796
particle.weight *= exp(-sigma_t*(pB-pA).Norm()); //Attenuation
797
particle.position = end_of_track_position;
798
\endcode
799
800
801
802
\section CodeTut93Sec7 7 Post-processing the tallies
803
The tallies up to this point are still in raw format. We need to convert them
804
to the project fashion we want.
805
\code
806
for (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
846
The first portion of the loop is simply housekeeping again
847
\code
848
for (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
865
We get the cell mapping, quadrature point data, and we build the mass matrix.
866
Recall 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]
870
which requires us to solve the small system
871
\f[
872
M \boldsymbol{\phi}^{uc} = \mathbf{T}
873
\f]
874
where \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
876
matrix we just directly invert it to be used for all groups and moments.
877
878
Next we loop over all moments and groups.
879
\code
880
VecDbl T(num_nodes, 0.0);
881
for (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
900
Within the moment and group loop we first set the entries of \f$ T \f$ to the
901
normalized 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
903
data and set the nodal values of the tally to the uncollided projected flux.
904
905
\section CodeTut93Sec8 8 Exporting field functions
906
Creating the field functions is similar to what we did in previous tutorials
907
\code
908
//============================================= Create Field Functions
909
std::vector<std::shared_ptr<chi_physics::FieldFunction>> ff_list;
910
911
ff_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
920
const chi_math::UnknownManager m0_uk_man(
921
{chi_math::Unknown(chi_math::UnknownType::VECTOR_N,num_groups)});
922
const size_t num_m0_dofs = sdm.GetNumLocalDOFs(m0_uk_man);
923
924
std::vector<double> m0_phi(num_m0_dofs, 0.0);
925
926
sdm.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
933
ff_list[0]->UpdateFieldVector(m0_phi);
934
935
936
//============================================= Update field function
937
chi_physics::FieldFunction::FFList const_ff_list;
938
for (const auto& ff_ptr : ff_list)
939
const_ff_list.push_back(ff_ptr);
940
chi_physics::FieldFunction::ExportMultipleToVTK(fname,
941
const_ff_list);
942
\endcode
943
944
The visualization below shows a logarithmic scale warp of the flux values for
945
both the uncollided algorithm and a LBS simulation using a product quadrature
946
with 96 azimuthal angles and 48 polar angles per octant (18,432 directions total).
947
948
The left of the figure is the uncollided algorithm and the right is LBS. Notice
949
the 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
970
namespace chi_unit_sim_tests
971
{
972
973
int 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
*/
doc
PAGES
ProgrammersManual
CodingTutorials
CodeTut93.h
Generated by
1.9.3