Loading...
Searching...
No Matches
radial_profiles.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <numeric>
4
8#include "util/table.hpp"
9
10namespace terra::shell {
11
12/// @brief Simple struct, holding device-views for radial profiles.
13///
14/// See `radial_profiles()` and `radial_profiles_to_table()` for radial profiles computation.
15template < typename ScalarType >
17{
18 explicit RadialProfiles( int radial_shells )
19 : radial_min_( "radial_profiles_min", radial_shells )
20 , radial_max_( "radial_profiles_max", radial_shells )
21 , radial_sum_( "radial_profiles_sum", radial_shells )
22 , radial_cnt_( "radial_profiles_cnt", radial_shells )
23 , radial_avg_( "radial_profiles_avg", radial_shells )
24 {}
25
31};
32
33/// @brief Compute radial profiles (min, max, sum, count, average) for a Q1 scalar field (on device!).
34///
35/// @details Computes the radial profiles for a Q1 scalar field by iterating over all radial shells.
36/// The profiles include minimum, maximum, sum, and count of nodes in each shell.
37/// The radial shells are defined by the radial dimension of the Q1 scalar field.
38/// The output is a RadialProfiles struct with device-side arrays of size num_shells which contain:
39/// - Minimum value in the shell
40/// - Maximum value in the shell
41/// - Sum of values in the shell
42/// - Count of nodes in the shell
43/// - Average of nodes in the shell (sum / count)
44/// Performs reduction per process on the device and also inter-device reduction with MPI_Allreduce.
45/// All processes will carry the same result.
46///
47/// Mask data is used internally to filter out non-owned nodes. So this really only reduces owned nodes.
48///
49/// @note The returned Views are still on the device.
50/// To convert it to a util::Table for output, use the `radial_profiles_to_table()` function.
51/// So unless you really want to compute any further data on the device using the profile, you like always want
52/// to call the `radial_profiles_to_table()` function as outlined below.
53///
54/// To nicely format the output, use the `radial_profiles_to_table()` function, e.g., via
55/// @code
56/// auto radii = domain_info.radii(); // the DomainInfo is also available in the DistributedDomain
57/// auto table = radial_profiles_to_table( radial_profiles( some_field_like_temperature ), radii );
58/// std::ofstream out( "radial_profiles.csv" );
59/// table.print_csv( out );
60/// @endcode
61///
62/// @tparam ScalarType Scalar type of the field.
63/// @param data Q1 scalar field data.
64/// @param subdomain_shell_idx global shell indices array with layout
65/// @code subdomain_shell_idx( local_subdomain_id, node_r_idx ) = global_shell_idx @endcode
66/// compute with \ref terra::grid::shell::subdomain_shell_idx.
67/// @param num_global_shells number of global shells
68/// @return RadialProfiles struct containing [min, max, sum, count, average] for each radial shell (still on the device).
69template < typename ScalarType >
72 const grid::Grid2DDataScalar< int >& subdomain_shell_idx,
73 const int num_global_shells )
74{
76
77 const auto data_grid = data.grid_data();
78 const auto data_mask = data.mask_data();
79
80 if ( data_grid.extent( 0 ) != subdomain_shell_idx.extent( 0 ) ||
81 data_grid.extent( 3 ) != subdomain_shell_idx.extent( 1 ) )
82 {
83 Kokkos::abort( "radial_profiles: Data and subdomain_shell_idx do not have matching dimensions." );
84 }
85
86 Kokkos::parallel_for(
87 "radial profiles init", num_global_shells, KOKKOS_LAMBDA( int r ) {
88 radial_profiles.radial_min_( r ) = Kokkos::Experimental::finite_max_v< ScalarType >;
89 radial_profiles.radial_max_( r ) = Kokkos::Experimental::finite_min_v< ScalarType >;
90 radial_profiles.radial_sum_( r ) = 0;
91 radial_profiles.radial_cnt_( r ) = 0;
92 } );
93
94 Kokkos::fence();
95
96 Kokkos::parallel_for(
97 "radial profiles reduction",
98 Kokkos::MDRangePolicy(
99 { 0, 0, 0, 0 },
100 { data_grid.extent( 0 ), data_grid.extent( 1 ), data_grid.extent( 2 ), data_grid.extent( 3 ) } ),
101 KOKKOS_LAMBDA( int local_subdomain_id, int x, int y, int r ) {
102 if ( !util::has_flag( data_mask( local_subdomain_id, x, y, r ), grid::NodeOwnershipFlag::OWNED ) )
103 {
104 return;
105 }
106
107 const int global_shell_idx = subdomain_shell_idx( local_subdomain_id, r );
108
109 Kokkos::atomic_min(
110 &radial_profiles.radial_min_( global_shell_idx ), data_grid( local_subdomain_id, x, y, r ) );
111 Kokkos::atomic_max(
112 &radial_profiles.radial_max_( global_shell_idx ), data_grid( local_subdomain_id, x, y, r ) );
113 Kokkos::atomic_add(
114 &radial_profiles.radial_sum_( global_shell_idx ), data_grid( local_subdomain_id, x, y, r ) );
115 Kokkos::atomic_add( &radial_profiles.radial_cnt_( global_shell_idx ), static_cast< ScalarType >( 1 ) );
116 } );
117
118 Kokkos::fence();
119
120 MPI_Allreduce(
121 MPI_IN_PLACE,
122 radial_profiles.radial_min_.data(),
123 radial_profiles.radial_min_.size(),
124 mpi::mpi_datatype< ScalarType >(),
125 MPI_MIN,
126 MPI_COMM_WORLD );
127
128 MPI_Allreduce(
129 MPI_IN_PLACE,
130 radial_profiles.radial_max_.data(),
131 radial_profiles.radial_max_.size(),
132 mpi::mpi_datatype< ScalarType >(),
133 MPI_MAX,
134 MPI_COMM_WORLD );
135
136 MPI_Allreduce(
137 MPI_IN_PLACE,
138 radial_profiles.radial_sum_.data(),
139 radial_profiles.radial_sum_.size(),
140 mpi::mpi_datatype< ScalarType >(),
141 MPI_SUM,
142 MPI_COMM_WORLD );
143
144 MPI_Allreduce(
145 MPI_IN_PLACE,
146 radial_profiles.radial_cnt_.data(),
147 radial_profiles.radial_cnt_.size(),
148 mpi::mpi_datatype< ScalarType >(),
149 MPI_SUM,
150 MPI_COMM_WORLD );
151
152 Kokkos::parallel_for(
153 "radial profiles avg", num_global_shells, KOKKOS_LAMBDA( int r ) {
154 radial_profiles.radial_avg_( r ) = radial_profiles.radial_sum_( r ) / radial_profiles.radial_cnt_( r );
155 } );
156
157 Kokkos::fence();
158
159 return radial_profiles;
160}
161
162/// @brief Convert radial profile data to a util::Table for analysis or output.
163///
164/// @details Converts the radial profile data (min, max, sum, avg, count) into a util::Table.
165///
166/// This table can then be used for further analysis or output to CSV/JSON.
167/// The table will have the following columns:
168/// - tag: "radial_profiles"
169/// - shell_idx: Index of the radial shell
170/// - radius: Radius of the shell
171/// - min: Minimum value in the shell
172/// - max: Maximum value in the shell
173/// - sum: Average value in the shell
174/// - avg: Average value in the shell
175/// - cnt: Count of nodes in the shell
176///
177/// To use this function, you can compute the radial profiles using `radial_profiles()` and then convert
178/// the result to a table using this function:
179///
180/// @code
181/// auto radii = domain_info.radii(); // the DomainInfo is also available in the DistributedDomain
182/// auto table = radial_profiles_to_table( radial_profiles( some_field_like_temperature ), radii );
183/// std::ofstream out( "radial_profiles.csv" );
184/// table.print_csv( out );
185/// @endcode
186///
187/// @tparam ScalarType Scalar type of the profile data.
188/// @param radial_profiles RadialProfiles struct containing radial profile statistics.
189/// Compute this using `radial_profiles()` function. Data is expected to be on the device still.
190/// It is copied to host for table creation in this function.
191/// @param radii Vector of shell radii. Can for instance be obtained from the DomainInfo.
192/// @return Table with columns: tag, shell_idx, radius, min, max, avg, cnt.
193template < typename ScalarType >
195 radial_profiles_to_table( const RadialProfiles< ScalarType >& radial_profiles, const std::vector< double >& radii )
196{
197 if ( radii.size() != radial_profiles.radial_min_.extent( 0 ) ||
198 radii.size() != radial_profiles.radial_max_.extent( 0 ) ||
199 radii.size() != radial_profiles.radial_sum_.extent( 0 ) ||
200 radii.size() != radial_profiles.radial_cnt_.extent( 0 ) )
201 {
202 throw std::runtime_error( "Radial profiles and radii do not have the same number of shells." );
203 }
204
205 const auto radial_profiles_host_min =
206 Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace(), radial_profiles.radial_min_ );
207 const auto radial_profiles_host_max =
208 Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace(), radial_profiles.radial_max_ );
209 const auto radial_profiles_host_sum =
210 Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace(), radial_profiles.radial_sum_ );
211 const auto radial_profiles_host_cnt =
212 Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace(), radial_profiles.radial_cnt_ );
213 const auto radial_profiles_host_avg =
214 Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace(), radial_profiles.radial_avg_ );
215
216 util::Table table;
217 for ( int r = 0; r < radii.size(); r++ )
218 {
219 table.add_row(
220 { { "tag", "radial_profiles" },
221 { "shell_idx", r },
222 { "radius", radii[r] },
223 { "min", radial_profiles_host_min( r ) },
224 { "max", radial_profiles_host_max( r ) },
225 { "sum", radial_profiles_host_sum( r ) },
226 { "avg", radial_profiles_host_avg( r ) },
227 { "cnt", radial_profiles_host_cnt( r ) } } );
228 }
229 return table;
230}
231
232/// @brief Linearly interpolates radial data from a std::vector (host) into a 2D grid (device) with dimensions
233/// (local_subdomain_id, subdomain_size_r) for straightforward use in kernels.
234///
235/// This function takes care of handling possibly duplicated nodes at subdomain boundaries.
236///
237/// @note This will clamp values outside the passed radial profile values to the first and last values in the vector
238/// respectively.
239///
240/// @param profile_out_label label of the returned Kokkos::View
241/// @param coords_radii radii of each node for all local subdomains - see \ref terra::grid::shell::subdomain_shell_radii(),
242/// the output grid data will have the same extents
243/// @param profile_radii input profile: vector of radii
244/// @param profile_values input profile: vector of values to interpolate
245/// @return Kokkos::View with the same dimensions of coords_radii, populated with interpolated values per subdomain
246template <
247 std::floating_point GridDataType,
248 std::floating_point ProfileInRadiiType,
249 std::floating_point ProfileInValueType,
250 std::floating_point ProfileOutDataType = double >
252 const std::string& profile_out_label,
253 const grid::Grid2DDataScalar< GridDataType >& coords_radii,
254 const std::vector< ProfileInRadiiType >& profile_radii,
255 const std::vector< ProfileInValueType >& profile_values )
256{
258 profile_out_label, coords_radii.extent( 0 ), coords_radii.extent( 1 ) );
259 auto profile_data_host = Kokkos::create_mirror_view( Kokkos::HostSpace{}, profile_data );
260
261 auto coords_radii_host = Kokkos::create_mirror_view_and_copy( Kokkos::HostSpace{}, coords_radii );
262
263 // Sort ascending (by radius - from innermost to outermost shell).
264
265 std::vector< int > idx( profile_radii.size() );
266 std::iota( idx.begin(), idx.end(), 0 );
267
268 // sort indices by comparing keys
269 std::sort( idx.begin(), idx.end(), [&]( int a, int b ) { return profile_radii[a] < profile_radii[b]; } );
270
271 // apply permutation
272 std::vector< ProfileInRadiiType > profile_radii_sorted( profile_radii );
273 std::vector< ProfileInValueType > profile_values_sorted( profile_values );
274
275 for ( size_t i = 0; i < idx.size(); i++ )
276 {
277 profile_radii_sorted[i] = profile_radii[idx[i]];
278 profile_values_sorted[i] = profile_values[idx[i]];
279 }
280
281 for ( int local_subdomain_id = 0; local_subdomain_id < profile_data_host.extent( 0 ); local_subdomain_id++ )
282 {
283 const size_t subdomain_size_r = profile_data_host.extent( 1 );
284 const size_t input_profile_size = profile_radii_sorted.size();
285
286 if ( input_profile_size == 0 )
287 {
288 continue;
289 }
290
291 if ( input_profile_size == 1 )
292 {
293 for ( int r = 0; r < subdomain_size_r; r++ )
294 {
295 profile_data_host( local_subdomain_id, r ) = profile_values_sorted[0];
296 }
297 continue;
298 }
299
300 size_t i = 0;
301
302 for ( size_t r = 0; r < subdomain_size_r; ++r )
303 {
304 double x = coords_radii_host( local_subdomain_id, r );
305
306 // Advance src index to the correct interval
307 while ( i + 1 < input_profile_size && profile_radii_sorted[i + 1] < x )
308 {
309 ++i;
310 }
311
312 // If x is below the first point → clamp
313 if ( x <= profile_radii_sorted.front() )
314 {
315 profile_data_host( local_subdomain_id, r ) = profile_values_sorted.front();
316 continue;
317 }
318
319 // If x is above the last point → clamp
320 if ( x >= profile_radii_sorted.back() )
321 {
322 profile_data_host( local_subdomain_id, r ) = profile_values_sorted.back();
323 continue;
324 }
325
326 // Interpolate between i and i+1
327 profile_data_host( local_subdomain_id, r ) = util::interpolate_linear_1D(
328 profile_radii_sorted[i],
329 profile_radii_sorted[i + 1],
330 profile_values_sorted[i],
331 profile_values_sorted[i + 1],
332 x,
333 true );
334 }
335 }
336
337 Kokkos::deep_copy( profile_data, profile_data_host );
338
339 return profile_data;
340}
341
342/// @brief Interpolating a radial profile from a CSV file into a grid.
343///
344/// This is just a convenient wrapper around \ref interpolate_radial_profile_into_subdomains() that also
345/// reads the CSV file.
346///
347/// @note This will clamp values outside the passed radial profile values to the first and last values in the vector
348/// respectively.
349///
350/// @param filename csv file name containing the radial profile (radii and at least one value column)
351/// @param key_radii name of the radii column
352/// @param key_values name of the value column
353/// @param coords_radii radii of each node for all local subdomains - see \ref terra::grid::shell::subdomain_shell_radii(),
354/// the output grid data will have the same extents
355/// @return Kokkos::View with the same dimensions of coords_radii, populated with interpolated values per subdomain
356template < std::floating_point GridDataType, std::floating_point ProfileOutDataType = double >
358 const std::string& filename,
359 const std::string& key_radii,
360 const std::string& key_values,
361 const grid::Grid2DDataScalar< GridDataType >& coords_radii )
362{
363 auto profile_table_result = util::read_table_from_csv( filename );
364 if ( profile_table_result.is_err() )
365 {
366 util::logroot << profile_table_result.error() << std::endl;
367 Kokkos::abort( "Error reading csv file into table." );
368 }
369 const auto& profile_table = profile_table_result.unwrap();
370
371 const auto profile_radii = profile_table.column_as_vector< double >( key_radii );
372 const auto profile_values = profile_table.column_as_vector< double >( key_values );
373
375 "radial_profile_" + key_values, coords_radii, profile_radii, profile_values );
376}
377
378/// @brief Interpolating a constant radial profile into a grid.
379///
380/// @param coords_radii radii of each node for all local subdomains - see \ref terra::grid::shell::subdomain_shell_radii(),
381/// the output grid data will have the same extents
382/// @param value the constant value to interpolate
383/// @return Kokkos::View with the same dimensions of coords_radii, populated with interpolated values per subdomain
384template < std::floating_point GridDataType, std::floating_point ProfileOutDataType = double >
386 const grid::Grid2DDataScalar< GridDataType >& coords_radii,
387 const ProfileOutDataType& value )
388{
390 "radial_profile_constant", coords_radii.extent( 0 ), coords_radii.extent( 1 ) );
391 kernels::common::set_constant( profile_data, value );
392 return profile_data;
393}
394
395} // namespace terra::shell
const grid::Grid4DDataScalar< ScalarType > & grid_data() const
Get const reference to grid data.
Definition vector_q1.hpp:137
const grid::Grid4DDataScalar< grid::NodeOwnershipFlag > & mask_data() const
Get const reference to mask data.
Definition vector_q1.hpp:142
Table class for storing and manipulating tabular data.
Definition table.hpp:84
void add_row(const Row &row_data)
Add a row to the table. Adds "id" and "timestamp" columns automatically.
Definition table.hpp:140
Kokkos::View< ScalarType *, Layout > Grid1DDataScalar
Definition grid_types.hpp:16
Kokkos::View< ScalarType **, Layout > Grid2DDataScalar
Definition grid_types.hpp:19
void set_constant(const grid::Grid2DDataScalar< ScalarType > &x, ScalarType value)
Definition grid_operations.hpp:12
Definition radial_profiles.hpp:10
grid::Grid2DDataScalar< ProfileOutDataType > interpolate_constant_radial_profile(const grid::Grid2DDataScalar< GridDataType > &coords_radii, const ProfileOutDataType &value)
Interpolating a constant radial profile into a grid.
Definition radial_profiles.hpp:385
util::Table radial_profiles_to_table(const RadialProfiles< ScalarType > &radial_profiles, const std::vector< double > &radii)
Convert radial profile data to a util::Table for analysis or output.
Definition radial_profiles.hpp:195
grid::Grid2DDataScalar< ProfileOutDataType > interpolate_radial_profile_into_subdomains_from_csv(const std::string &filename, const std::string &key_radii, const std::string &key_values, const grid::Grid2DDataScalar< GridDataType > &coords_radii)
Interpolating a radial profile from a CSV file into a grid.
Definition radial_profiles.hpp:357
RadialProfiles< ScalarType > radial_profiles(const linalg::VectorQ1Scalar< ScalarType > &data, const grid::Grid2DDataScalar< int > &subdomain_shell_idx, const int num_global_shells)
Compute radial profiles (min, max, sum, count, average) for a Q1 scalar field (on device!...
Definition radial_profiles.hpp:70
grid::Grid2DDataScalar< ProfileOutDataType > interpolate_radial_profile_into_subdomains(const std::string &profile_out_label, const grid::Grid2DDataScalar< GridDataType > &coords_radii, const std::vector< ProfileInRadiiType > &profile_radii, const std::vector< ProfileInValueType > &profile_values)
Linearly interpolates radial data from a std::vector (host) into a 2D grid (device) with dimensions (...
Definition radial_profiles.hpp:251
Result< Table > read_table_from_csv(const std::string &filename)
Attempts to read a csv file and converts that into a Table instance.
Definition table.hpp:523
double interpolate_linear_1D(const double pos_a, const double pos_b, const double val_a, const double val_b, const double pos, const bool clamp)
Computes the linearly interpolated value at a specified point using two surrounding data points.
Definition interpolation.hpp:17
constexpr bool has_flag(E mask_value, E flag) noexcept
Checks if a bitmask value contains a specific flag.
Definition bit_masking.hpp:43
detail::PrefixCout logroot([]() { return detail::log_prefix();})
std::ostream subclass that just logs on root and adds a timestamp for each line.
Simple struct, holding device-views for radial profiles.
Definition radial_profiles.hpp:17
grid::Grid1DDataScalar< ScalarType > radial_cnt_
Definition radial_profiles.hpp:29
grid::Grid1DDataScalar< ScalarType > radial_sum_
Definition radial_profiles.hpp:28
grid::Grid1DDataScalar< ScalarType > radial_avg_
Definition radial_profiles.hpp:30
grid::Grid1DDataScalar< ScalarType > radial_min_
Definition radial_profiles.hpp:26
RadialProfiles(int radial_shells)
Definition radial_profiles.hpp:18
grid::Grid1DDataScalar< ScalarType > radial_max_
Definition radial_profiles.hpp:27