ryujin 2.1.1 revision ee5cbcbf2346c1299c942d0e1f13b46449973c18
Loading...
Searching...
No Matches
quantities.template.h
Go to the documentation of this file.
1//
2// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
3// Copyright (C) 2020 - 2026 by the ryujin authors
4//
5
6#pragma once
7
8#include "computing_timer.h"
9#include "quantities.h"
10
11#include <deal.II/base/function_parser.h>
12#include <deal.II/base/mpi.templates.h>
13#include <deal.II/base/work_stream.h>
14#include <deal.II/dofs/dof_tools.h>
15
16#include <fstream>
17
18DEAL_II_NAMESPACE_OPEN
19template <int rank, int dim, typename Number>
20bool operator<(const Tensor<rank, dim, Number> &left,
21 const Tensor<rank, dim, Number> &right)
22{
23 return std::lexicographical_compare(
24 left.begin_raw(), left.end_raw(), right.begin_raw(), right.end_raw());
25}
26DEAL_II_NAMESPACE_CLOSE
27
28namespace ryujin
29{
30 using namespace dealii;
31
32 namespace
33 {
34 template <typename T>
35 const std::string &get_options_from_name(const T &manifolds,
36 const std::string &name)
37 {
38 const auto it =
39 std::find_if(manifolds.begin(),
40 manifolds.end(),
41 [&, name = std::cref(name)](const auto &element) {
42 return std::get<0>(element) == name.get();
43 });
44 Assert(it != manifolds.end(), dealii::ExcInternalError());
45 return std::get<2>(*it);
46 }
47 } // namespace
48
49
50 template <typename Description, int dim, typename Number>
52 const MPIEnsemble &mpi_ensemble,
53 const OfflineData<dim, Number> &offline_data,
54 const HyperbolicSystem &hyperbolic_system,
55 const ParabolicSystem &parabolic_system,
56 const std::string &subsection /*= "Quantities"*/)
57 : ParameterAcceptor(subsection)
58 , mpi_ensemble_(mpi_ensemble)
59 , offline_data_(&offline_data)
60 , hyperbolic_system_(&hyperbolic_system)
61 , parabolic_system_(&parabolic_system)
62 , base_name_("")
63 , mesh_files_have_been_written_(false)
64 {
65 add_parameter("interior manifolds",
66 interior_manifolds_,
67 "List of level set functions describing interior manifolds. "
68 "The description is used to only output point values for "
69 "vertices belonging to a certain level set. "
70 "Format: '<name> : <level set formula> : <options> , [...] "
71 "(options: time_averaged, space_averaged, instantaneous)");
72
73 add_parameter("boundary manifolds",
74 boundary_manifolds_,
75 "List of level set functions describing boundary. The "
76 "description is used to only output point values for "
77 "boundary vertices belonging to a certain level set. "
78 "Format: '<name> : <level set formula> : <options> , [...] "
79 "(options: time_averaged, space_averaged, instantaneous)");
80
81 clear_temporal_statistics_on_writeout_ = true;
82 add_parameter("clear statistics on writeout",
83 clear_temporal_statistics_on_writeout_,
84 "If set to true then all temporal statistics (for "
85 "\"time_averaged\" quantities) accumulated so far are reset "
86 "each time a writeout of quantities is performed");
87 }
88
89
90 template <typename Description, int dim, typename Number>
92 {
93#ifdef DEBUG_OUTPUT
94 std::cout << "Quantities<dim, Number>::prepare()" << std::endl;
95#endif
96
97 base_name_ = name;
98
99 /* Force to write to a new time series file: */
100 time_series_cycle_.reset();
101
102 const unsigned int n_owned = offline_data_->n_locally_owned();
103 const auto sparsity_simd_view =
104 offline_data_->sparsity_pattern_simd().view();
105 const auto lumped_mass_matrix_view =
106 offline_data_->lumped_mass_matrix().view();
107
108 /*
109 * Create interior maps and allocate statistics.
110 *
111 * We have to loop over the cells and populate the std::map interior_maps_.
112 */
113
114 interior_maps_.clear();
115 std::transform(
116 interior_manifolds_.begin(),
117 interior_manifolds_.end(),
118 std::inserter(interior_maps_, interior_maps_.end()),
119 [this, n_owned, &sparsity_simd_view, &lumped_mass_matrix_view](
120 auto it) {
121 const auto &[name, expression, option] = it;
122 FunctionParser<dim> level_set_function(expression);
123
124 std::vector<interior_point> map;
125 std::map<int, interior_point> preliminary_map;
126
127 const auto &discretization = offline_data_->discretization();
128 const auto &dof_handler = offline_data_->dof_handler();
129
130 const auto support_points =
131 dof_handler.get_fe().get_unit_support_points();
132
133 std::vector<dealii::types::global_dof_index> local_dof_indices;
134
135 /* Loop over cells */
136 for (auto cell : dof_handler.active_cell_iterators()) {
137
138 /* skip if not locally owned */
139 if (!cell->is_locally_owned())
140 continue;
141
142 const unsigned int dofs_per_cell = cell->get_fe().n_dofs_per_cell();
143 local_dof_indices.resize(dofs_per_cell);
144 cell->get_active_or_mg_dof_indices(local_dof_indices);
145
146 const auto &mapping =
147 discretization.mapping()[cell->active_fe_index()];
148
149 for (unsigned int j = 0; j < dofs_per_cell; ++j) {
150
151 Point<dim> position =
152 mapping.transform_unit_to_real_cell(cell, support_points[j]);
153
154 /*
155 * Insert index, interior mass value and position into
156 * a preliminary map if we satisfy level set condition.
157 */
158
159 if (std::abs(level_set_function.value(position)) > 1.e-12)
160 continue;
161
162 const auto global_index = local_dof_indices[j];
163 const auto index =
164 offline_data_->scalar_partitioner()->global_to_local(
165 global_index);
166
167 /* Skip constrained degrees of freedom: */
168 const unsigned int row_length =
169 sparsity_simd_view.row_length(index);
170 if (row_length == 1)
171 continue;
172
173 if (index >= n_owned)
174 continue;
175
176 const Number interior_mass =
177 lumped_mass_matrix_view.read_entry(index);
178 // FIXME: change to std::set
179 preliminary_map[index] = {index, interior_mass, position};
180 }
181 }
182
183 /*
184 * Now we populate the std::vector(interior_point) object called map.
185 */
186 // FIXME: use std::copy
187 for (const auto &[index, tuple] : preliminary_map) {
188 map.push_back(tuple);
189 }
190
191 return std::make_pair(name, map);
192 });
193
194 /*
195 * Create boundary maps and allocate statistics vector:
196 *
197 * We want to loop over the boundary_map() once and populate the map
198 * object boundary_maps_. We have to create a vector of
199 * boundary_manifolds.size() that holds a std::vector<boundary_point>
200 * for each map entry.
201 */
202
203 boundary_maps_.clear();
204 std::transform(
205 boundary_manifolds_.begin(),
206 boundary_manifolds_.end(),
207 std::inserter(boundary_maps_, boundary_maps_.end()),
208 [this, n_owned](auto it) {
209 const auto &[name, expression, option] = it;
210 FunctionParser<dim> level_set_function(expression);
211
212 std::vector<boundary_point> map;
213
214 for (const auto &entry : offline_data_->boundary_map()) {
215 // [i, normal, normal_mass, boundary_mass, id, position] = entry
216 const auto &i = std::get<0>(entry);
217
218 /* skip nonlocal */
219 if (i >= n_owned)
220 continue;
221
222 /* skip constrained */
223 if (offline_data_->affine_constraints().is_constrained(
224 offline_data_->scalar_partitioner()->local_to_global(i)))
225 continue;
226
227 const auto &position = std::get<5>(entry);
228 if (std::abs(level_set_function.value(position)) < 1.e-12)
229 map.push_back(entry);
230 }
231 return std::make_pair(name, map);
232 });
233
234 /* Clear statistics: */
235 clear_statistics();
236
237 /* Make sure we output new mesh files: */
238 mesh_files_have_been_written_ = false;
239
240 /* Prepare header string: */
241 const auto &names = View::primitive_component_names;
242 header_ = std::accumulate(
243 std::begin(names),
244 std::end(names),
245 std::string(),
246 [](const std::string &description, const std::string &name) {
247 return description.empty()
248 ? (std::string("primitive state (") + name)
249 : (description + ", " + name);
250 }) +
251 ")\t and 2nd moments\n";
252 }
253
254
255 template <typename Description, int dim, typename Number>
257 const StateVector &state_vector, const Number t)
258 {
259#ifdef DEBUG_OUTPUT
260 std::cout << "Quantities<dim, Number>::accumulate()" << std::endl;
261#endif
262
263 const auto accumulate = [&](const auto &point_maps,
264 const auto &manifolds,
265 auto &statistics,
266 auto &time_series) {
267 for (const auto &[name, point_map] : point_maps) {
268
269 /* Find the correct option string in manifolds */
270 const auto &options = get_options_from_name(manifolds, name);
271
272 /* skip if we don't average in space or time: */
273 if (options.find("time_averaged") == std::string::npos &&
274 options.find("space_averaged") == std::string::npos)
275 continue;
276
277 auto &[val_old, val_new, val_sum, t_old, t_new, t_sum] =
278 statistics[name];
279
280 std::swap(t_old, t_new);
281 std::swap(val_old, val_new);
282
283 /* accumulate new values */
284
285 const auto spatial_average =
286 internal_accumulate(state_vector, point_map, val_new);
287
288 /* Average in time with trapezoidal rule: */
289
290 if (RYUJIN_UNLIKELY(t_old == Number(0.) && t_new == Number(0.))) {
291 /* We have not accumulated any statistics yet: */
292 t_old = t - 1.;
293 t_new = t;
294
295 } else {
296
297 t_new = t;
298 const Number tau = t_new - t_old;
299
300 for (std::size_t i = 0; i < val_sum.size(); ++i) {
301 std::get<0>(val_sum[i]) += 0.5 * tau * std::get<0>(val_old[i]);
302 std::get<0>(val_sum[i]) += 0.5 * tau * std::get<0>(val_new[i]);
303 std::get<1>(val_sum[i]) += 0.5 * tau * std::get<1>(val_old[i]);
304 std::get<1>(val_sum[i]) += 0.5 * tau * std::get<1>(val_new[i]);
305 }
306 t_sum += tau;
307 }
308
309 /* Record average in space: */
310 time_series[name].push_back({t, spatial_average});
311 }
312 };
313
314 accumulate(interior_maps_,
315 interior_manifolds_,
316 interior_statistics_,
317 interior_time_series_);
318
319 accumulate(boundary_maps_,
320 boundary_manifolds_,
321 boundary_statistics_,
322 boundary_time_series_);
323 }
324
325
326 template <typename Description, int dim, typename Number>
328 const StateVector &state_vector, const Number t, unsigned int cycle)
329 {
330#ifdef DEBUG_OUTPUT
331 std::cout << "Quantities<dim, Number>::write_out()" << std::endl;
332#endif
333
334 /*
335 * First, write out mesh files if this hasn't happened yet.
336 */
337 if (!mesh_files_have_been_written_) {
338 write_mesh_files(cycle);
339 mesh_files_have_been_written_ = true;
340 }
341
342 /*
343 * Next write out instantaneous and time_averaged maps, and flush the
344 * space_averaged values to the corresponding log files:
345 */
346
347 const auto write_out = [&](const auto &point_maps,
348 const auto &manifolds,
349 auto &statistics,
350 auto &time_series) {
351 for (const auto &[name, point_map] : point_maps) {
352
353 /* Find the correct option string in manifolds */
354 const auto &options = get_options_from_name(manifolds, name);
355
356 const auto prefix =
357 base_name_ + "-" + name + "-R" + Utilities::to_string(cycle, 4);
358
359 /*
360 * Compute and output instantaneous field:
361 */
362
363 if (options.find("instantaneous") != std::string::npos) {
364
365 const std::string file_name = prefix + "-instantaneous.dat";
366
367 auto &[val_old, val_new, val_sum, t_old, t_new, t_sum] =
368 statistics[name];
369
370 std::stringstream time_stamp;
371 time_stamp << std::scientific << std::setprecision(14);
372 time_stamp << "# at t = " << t << std::endl;
373
374 /* We have not computed any updated statistics yet: */
375
376 if (options.find("time_averaged") == std::string::npos &&
377 options.find("space_averaged") == std::string::npos)
378 internal_accumulate(state_vector, point_map, val_new);
379 else
380 AssertThrow(t_new == t, dealii::ExcInternalError());
381
382 internal_write_out(file_name, time_stamp.str(), val_new, Number(1.));
383 }
384
385 /*
386 * Output time averaged field:
387 */
388
389 if (options.find("time_averaged") != std::string::npos) {
390
391 const std::string file_name = prefix + "-time_averaged.dat";
392
393 auto &[val_old, val_new, val_sum, t_old, t_new, t_sum] =
394 statistics[name];
395
396 /* Check whether we have accumulated any statistics yet: */
397 if (t_sum != Number(0.)) {
398 std::stringstream time_stamp;
399 time_stamp << std::scientific << std::setprecision(14);
400 time_stamp << "# averaged from t = " << t_new - t_sum
401 << " to t = " << t_new << std::endl;
402
403 internal_write_out(
404 file_name, time_stamp.str(), val_sum, Number(1.) / t_sum);
405 }
406 }
407
408 /*
409 * Output space averaged field:
410 */
411
412 if (options.find("space_averaged") != std::string::npos) {
413 bool append = true;
414 if (!time_series_cycle_.has_value()) {
415 time_series_cycle_ = cycle;
416 append = false;
417 }
418
419 const auto file_name =
420 base_name_ + "-" + name + "-R" +
421 Utilities::to_string(time_series_cycle_.value(), 4) +
422 "-space_averaged_time_series.dat";
423
424 auto &series = time_series[name];
425 internal_write_out_time_series(file_name, series, /*append*/ append);
426 series.clear();
427 }
428 }
429 };
430
431 write_out(interior_maps_,
432 interior_manifolds_,
433 interior_statistics_,
434 interior_time_series_);
435
436 write_out(boundary_maps_,
437 boundary_manifolds_,
438 boundary_statistics_,
439 boundary_time_series_);
440
441 if (clear_temporal_statistics_on_writeout_)
442 clear_statistics();
443 }
444
445
446 template <typename Description, int dim, typename Number>
447 void
449 {
450 /*
451 * Output interior maps:
452 */
453
454 for (const auto &[name, interior_map] : interior_maps_) {
455 /* Skip outputting the boundary map for spatial averages. */
456 const auto &options = get_options_from_name(interior_manifolds_, name);
457 if (options.find("instantaneous") == std::string::npos &&
458 options.find("time_averaged") == std::string::npos)
459 continue;
460
461 /*
462 * FIXME: This currently distributes boundary maps to all MPI ranks.
463 * This is unnecessarily wasteful. Ideally, we should do MPI IO with
464 * only MPI ranks participating who actually have boundary values.
465 */
466
467 const auto received = Utilities::MPI::gather(
468 mpi_ensemble_.ensemble_communicator(), interior_map);
469
470 if (Utilities::MPI::this_mpi_process(
471 mpi_ensemble_.ensemble_communicator()) == 0) {
472
473 std::ofstream output(base_name_ + "-" + name + "-R" +
474 Utilities::to_string(cycle, 4) + "-points.dat");
475
476 output << std::scientific << std::setprecision(14);
477
478 output << "#\n# position\tinterior mass\n";
479
480 unsigned int rank = 0;
481 for (const auto &entries : received) {
482 output << "# rank " << rank++ << "\n";
483 for (const auto &entry : entries) {
484 const auto &[index, mass_i, x_i] = entry;
485 output << x_i << "\t" << mass_i << "\n";
486 } /*entry*/
487 } /*entries*/
488
489 output << std::flush;
490 }
491 }
492
493 /*
494 * Output boundary maps:
495 */
496
497 for (const auto &[name, boundary_map] : boundary_maps_) {
498 /* Skip outputting the boundary map for spatial averages. */
499 const auto &options = get_options_from_name(boundary_manifolds_, name);
500 if (options.find("instantaneous") == std::string::npos &&
501 options.find("time_averaged") == std::string::npos)
502 continue;
503
504 /*
505 * FIXME: This currently distributes boundary maps to all MPI ranks.
506 * This is unnecessarily wasteful. Ideally, we should do MPI IO with
507 * only MPI ranks participating who actually have boundary values.
508 */
509
510 const auto received = Utilities::MPI::gather(
511 mpi_ensemble_.ensemble_communicator(), boundary_map);
512
513 if (Utilities::MPI::this_mpi_process(
514 mpi_ensemble_.ensemble_communicator()) == 0) {
515
516 std::ofstream output(base_name_ + "-" + name + "-R" +
517 Utilities::to_string(cycle, 4) + "-points.dat");
518
519 output << std::scientific << std::setprecision(14);
520
521 output << "#\n# position\tnormal\tnormal mass\tboundary mass\n";
522
523 unsigned int rank = 0;
524 for (const auto &entries : received) {
525 output << "# rank " << rank++ << "\n";
526 for (const auto &entry : entries) {
527 const auto &[index, n_i, nm_i, bm_i, id, x_i] = entry;
528 output << x_i << "\t" << n_i << "\t" << nm_i << "\t" << bm_i
529 << "\n";
530 } /*entry*/
531 } /*entries*/
532
533 output << std::flush;
534 }
535 }
536 }
537
538
539 template <typename Description, int dim, typename Number>
540 void Quantities<Description, dim, Number>::clear_statistics()
541 {
542 const auto reset = [](const auto &manifold_map, auto &statistics_map) {
543 for (const auto &[name, data_map] : manifold_map) {
544 const auto n_entries = data_map.size();
545 auto &[val_old, val_new, val_sum, t_old, t_new, t_sum] =
546 statistics_map[name];
547 val_old.resize(n_entries);
548 val_new.resize(n_entries);
549 val_sum.resize(n_entries);
550 t_old = t_new = t_sum = 0.;
551 }
552 };
553
554 /* Clear statistics and time series: */
555
556 interior_statistics_.clear();
557 reset(interior_maps_, interior_statistics_);
558 interior_time_series_.clear();
559
560 boundary_statistics_.clear();
561 reset(boundary_maps_, boundary_statistics_);
562 boundary_time_series_.clear();
563 }
564
565
566 template <typename Description, int dim, typename Number>
567 template <typename point_type, typename value_type>
568 value_type Quantities<Description, dim, Number>::internal_accumulate(
569 const StateVector &state_vector,
570 const std::vector<point_type> &points_vector,
571 std::vector<value_type> &val_new)
572 {
573 /* Ensure that the state vector is resident on the host memory space. */
574 if constexpr (have_separate_memory_spaces) {
575 ComputingTimer::Scope scope("time step [X] _ - memory space transfers");
576 const auto &[U, precomputed, parabolic] = state_vector;
577 U.template copy_to_memory_space<dealii::MemorySpace::Host>();
578 precomputed.template copy_to_memory_space<dealii::MemorySpace::Host>();
579 }
580
581 const auto U_view = std::get<0>(state_vector).view();
582
583 value_type spatial_average;
584 Number mass_sum = Number(0.);
585
586 std::transform(
587 points_vector.begin(),
588 points_vector.end(),
589 val_new.begin(),
590 [&](auto point) -> value_type {
591 const auto i = std::get<0>(point);
592 /*
593 * Small trick to get the correct index for retrieving the
594 * boundary mass.
595 */
596 constexpr auto index =
597 std::is_same_v<point_type, interior_point> ? 1 : 3;
598 const auto mass_i = std::get<index>(point);
599
600 const auto U_i = U_view.read_tensor(i);
601 const auto view = hyperbolic_system_->template view<dim, Number>();
602 const auto primitive_state = view.to_primitive_state(U_i);
603
604 value_type result;
605 std::get<0>(result) = primitive_state;
606 /* Compute second moments of the primitive state: */
607 std::get<1>(result) = schur_product(primitive_state, primitive_state);
608
609 mass_sum += mass_i;
610 std::get<0>(spatial_average) += mass_i * std::get<0>(result);
611 std::get<1>(spatial_average) += mass_i * std::get<1>(result);
612
613 return result;
614 });
615
616 /* synchronize MPI ranks (MPI Barrier): */
617
618 mass_sum =
619 Utilities::MPI::sum(mass_sum, mpi_ensemble_.ensemble_communicator());
620
621 std::get<0>(spatial_average) = Utilities::MPI::sum(
622 std::get<0>(spatial_average), mpi_ensemble_.ensemble_communicator());
623 std::get<1>(spatial_average) = Utilities::MPI::sum(
624 std::get<1>(spatial_average), mpi_ensemble_.ensemble_communicator());
625
626 /* take average: */
627
628 std::get<0>(spatial_average) /= mass_sum;
629 std::get<1>(spatial_average) /= mass_sum;
630
631 return spatial_average;
632 }
633
634
635 template <typename Description, int dim, typename Number>
636 template <typename value_type>
637 void Quantities<Description, dim, Number>::internal_write_out(
638 const std::string &file_name,
639 const std::string &time_stamp,
640 const std::vector<value_type> &values,
641 const Number scale)
642 {
643 /*
644 * FIXME: This currently distributes interior maps to all MPI ranks.
645 * This is unnecessarily wasteful. Ideally, we should do MPI IO with
646 * only MPI ranks participating who actually have interior values.
647 */
648
649 const auto received =
650 Utilities::MPI::gather(mpi_ensemble_.ensemble_communicator(), values);
651
652 if (Utilities::MPI::this_mpi_process(
653 mpi_ensemble_.ensemble_communicator()) == 0) {
654
655 std::ofstream output(file_name);
656 output << std::scientific << std::setprecision(14);
657 output << time_stamp << "# " << header_;
658
659 unsigned int rank = 0;
660 for (const auto &entries : received) {
661 output << "# rank " << rank++ << "\n";
662 for (const auto &entry : entries) {
663 const auto &[state, state_square] = entry;
664 output << scale * state << "\t" << scale * state_square << "\n";
665 } /*entry*/
666 } /*entries*/
667
668 output << std::flush;
669 }
670 }
671
672
673 template <typename Description, int dim, typename Number>
674 template <typename value_type>
675 void Quantities<Description, dim, Number>::internal_write_out_time_series(
676 const std::string &file_name,
677 const std::vector<std::tuple<Number, value_type>> &values,
678 bool append)
679 {
680 if (Utilities::MPI::this_mpi_process(
681 mpi_ensemble_.ensemble_communicator()) == 0) {
682 std::ofstream output;
683 output << std::scientific << std::setprecision(14);
684
685 if (append) {
686 output.open(file_name, std::ofstream::out | std::ofstream::app);
687 } else {
688 output.open(file_name, std::ofstream::out | std::ofstream::trunc);
689 output << "# time t\t" << header_;
690 }
691
692 for (const auto &entry : values) {
693 const auto t = std::get<0>(entry);
694 const auto &[state, state_square] = std::get<1>(entry);
695
696 output << t << "\t" << state << "\t" << state_square << "\n";
697 }
698
699 output << std::flush;
700 output.close();
701 }
702 }
703
704} /* namespace ryujin */
typename Description::HyperbolicSystem HyperbolicSystem
Definition quantities.h:38
typename Description::ParabolicSystem ParabolicSystem
Definition quantities.h:39
typename View::StateVector StateVector
Definition quantities.h:45
Quantities(const MPIEnsemble &mpi_ensemble, const OfflineData< dim, Number > &offline_data, const HyperbolicSystem &hyperbolic_system, const ParabolicSystem &parabolic_system, const std::string &subsection="/Quantities")
constexpr bool have_separate_memory_spaces
Definition gpu.h:29
#define RYUJIN_UNLIKELY(x)
DEAL_II_NAMESPACE_OPEN bool operator<(const Tensor< rank, dim, Number > &left, const Tensor< rank, dim, Number > &right)