Shamrock 2025.10.0
Astrophysical Code
Loading...
Searching...
No Matches
Solver.cpp
Go to the documentation of this file.
1// -------------------------------------------------------//
2//
3// SHAMROCK code for hydrodynamics
4// Copyright (c) 2021-2026 Timothée David--Cléris <tim.shamrock@proton.me>
5// SPDX-License-Identifier: CeCILL Free Software License Agreement v2.1
6// Shamrock is licensed under the CeCILL 2.1 License, see LICENSE for more information
7//
8// -------------------------------------------------------//
9
25
28#include "shambase/memory.hpp"
29#include "shambase/string.hpp"
30#include "shambase/time.hpp"
35#include "shambackends/math.hpp"
36#include "shamcomm/logs.hpp"
70#include <memory>
71#include <stdexcept>
72#include <vector>
73
74template<class Tvec, template<class> class Kern>
75void shammodels::gsph::Solver<Tvec, Kern>::init_solver_graph() {
76
77 storage.part_counts = std::make_shared<shamrock::solvergraph::Indexes<u32>>(
78 edges::part_counts, "N_{\\rm part}");
79
80 storage.part_counts_with_ghost = std::make_shared<shamrock::solvergraph::Indexes<u32>>(
81 edges::part_counts_with_ghost, "N_{\\rm part, with ghost}");
82
83 storage.patch_rank_owner = std::make_shared<shamrock::solvergraph::RankGetter>(
84 [&](u64 patch_id) -> u32 {
85 return scheduler().get_patch_rank_owner(patch_id);
86 },
87 "patch_rank_owner",
88 "rank");
89
90 // Merged ghost spans
91 storage.positions_with_ghosts = std::make_shared<shamrock::solvergraph::FieldRefs<Tvec>>(
92 edges::positions_with_ghosts, "\\mathbf{r}");
93 storage.hpart_with_ghosts
94 = std::make_shared<shamrock::solvergraph::FieldRefs<Tscal>>(edges::hpart_with_ghosts, "h");
95
96 storage.neigh_cache
97 = std::make_shared<shammodels::sph::solvergraph::NeighCache>(edges::neigh_cache, "neigh");
98
99 // Register ghost handler in solvergraph for explicit data dependency tracking
100 storage.ghost_handler = storage.solver_graph.register_edge(
101 "ghost_handler", solvergraph::GhostHandlerEdge<Tvec>("ghost_handler", "\\mathcal{G}"));
102
103 storage.omega = std::make_shared<shamrock::solvergraph::Field<Tscal>>(1, "omega", "\\Omega");
104 storage.density = std::make_shared<shamrock::solvergraph::Field<Tscal>>(1, "density", "\\rho");
105 storage.pressure = std::make_shared<shamrock::solvergraph::Field<Tscal>>(1, "pressure", "P");
106 storage.soundspeed
107 = std::make_shared<shamrock::solvergraph::Field<Tscal>>(1, "soundspeed", "c_s");
108
109 // Initialize gradient fields for MUSCL reconstruction
110 // These are only used when reconstruct_config.is_muscl() == true
111 storage.grad_density
112 = std::make_shared<shamrock::solvergraph::Field<Tvec>>(1, "grad_density", "\\nabla\\rho");
113 storage.grad_pressure
114 = std::make_shared<shamrock::solvergraph::Field<Tvec>>(1, "grad_pressure", "\\nabla P");
115 storage.grad_vx
116 = std::make_shared<shamrock::solvergraph::Field<Tvec>>(1, "grad_vx", "\\nabla v_x");
117 storage.grad_vy
118 = std::make_shared<shamrock::solvergraph::Field<Tvec>>(1, "grad_vy", "\\nabla v_y");
119 storage.grad_vz
120 = std::make_shared<shamrock::solvergraph::Field<Tvec>>(1, "grad_vz", "\\nabla v_z");
121}
122
123template<class Tvec, template<class> class Kern>
124void shammodels::gsph::Solver<Tvec, Kern>::vtk_do_dump(
125 std::string filename, bool add_patch_world_id) {
126
127 modules::VTKDump<Tvec, Kern>(context, solver_config).do_dump(filename, add_patch_world_id);
128}
129
130template<class Tvec, template<class> class Kern>
131void shammodels::gsph::Solver<Tvec, Kern>::gen_serial_patch_tree() {
132 StackEntry stack_loc{};
133
134 SerialPatchTree<Tvec> _sptree = SerialPatchTree<Tvec>::build(scheduler());
135 _sptree.attach_buf();
136 storage.serial_patch_tree.set(std::move(_sptree));
137}
138
139template<class Tvec, template<class> class Kern>
140void shammodels::gsph::Solver<Tvec, Kern>::gen_ghost_handler(Tscal time_val) {
141 StackEntry stack_loc{};
142
143 using CfgClass = gsph::GSPHGhostHandlerConfig<Tvec>;
144 using BCConfig = typename CfgClass::Variant;
145
146 using BCFree = typename CfgClass::Free;
147 using BCPeriodic = typename CfgClass::Periodic;
148 using BCShearingPeriodic = typename CfgClass::ShearingPeriodic;
149
150 using SolverConfigBC = typename Config::BCConfig;
151 using SolverBCFree = typename SolverConfigBC::Free;
152 using SolverBCPeriodic = typename SolverConfigBC::Periodic;
153 using SolverBCShearingPeriodic = typename SolverConfigBC::ShearingPeriodic;
154
155 // Boundary condition selection - similar to SPH solver
156 // Note: Wall boundaries use Periodic with dynamic wall particles
157 if (SolverBCFree *c = std::get_if<SolverBCFree>(&solver_config.boundary_config.config)) {
158 shambase::get_check_ref(storage.ghost_handler)
159 .set(
160 GhostHandle{
161 scheduler(), BCFree{}, storage.patch_rank_owner, storage.xyzh_ghost_layout});
162 } else if (
163 SolverBCPeriodic *c
164 = std::get_if<SolverBCPeriodic>(&solver_config.boundary_config.config)) {
165 shambase::get_check_ref(storage.ghost_handler)
166 .set(
167 GhostHandle{
168 scheduler(),
169 BCPeriodic{},
170 storage.patch_rank_owner,
171 storage.xyzh_ghost_layout});
172 } else if (
173 SolverBCShearingPeriodic *c
174 = std::get_if<SolverBCShearingPeriodic>(&solver_config.boundary_config.config)) {
175 // Shearing periodic boundaries (Stone 2010) - reuse SPH implementation
176 shambase::get_check_ref(storage.ghost_handler)
177 .set(
178 GhostHandle{
179 scheduler(),
180 BCShearingPeriodic{
181 c->shear_base, c->shear_dir, c->shear_speed * time_val, c->shear_speed},
182 storage.patch_rank_owner,
183 storage.xyzh_ghost_layout});
184 } else {
185 shambase::throw_with_loc<std::runtime_error>("GSPH: Unsupported boundary condition type.");
186 }
187}
188
189template<class Tvec, template<class> class Kern>
190void shammodels::gsph::Solver<Tvec, Kern>::build_ghost_cache() {
191 StackEntry stack_loc{};
192
193 using GSPHUtils = GSPHUtilities<Tvec, Kernel>;
194 GSPHUtils gsph_utils(scheduler());
195
196 // Same widening as compute_presteps_rint()/start_neighbors_cache(): with
197 // InutsukaV2 the kernel support is sqrt(2)*h, so the patch/rank ghost
198 // interface radius must widen too, or particles near a patch boundary
199 // could be missing valid neighbors from the adjacent patch.
200 Tscal h_evol_max = solver_config.htol_up_coarse_cycle;
201 if (solver_config.is_force_inutsuka_v2()) {
202 h_evol_max *= shambase::constants::sqrt_2<Tscal>;
203 }
204
205 storage.ghost_patch_cache.set(gsph_utils.build_interf_cache(
206 shambase::get_check_ref(storage.ghost_handler).get(),
207 storage.serial_patch_tree.get(),
208 h_evol_max));
209}
210
211template<class Tvec, template<class> class Kern>
212void shammodels::gsph::Solver<Tvec, Kern>::clear_ghost_cache() {
213 StackEntry stack_loc{};
214 storage.ghost_patch_cache.reset();
215}
216
217template<class Tvec, template<class> class Kern>
218void shammodels::gsph::Solver<Tvec, Kern>::merge_position_ghost() {
219 StackEntry stack_loc{};
220
221 storage.merged_xyzh.set(
222 shambase::get_check_ref(storage.ghost_handler)
223 .get()
224 .build_comm_merge_positions(storage.ghost_patch_cache.get()));
225
226 // Get field indices from xyzh_ghost_layout
227 const u32 ixyz_ghost
228 = storage.xyzh_ghost_layout->template get_field_idx<Tvec>(gsph::names::common::xyz);
229 const u32 ihpart_ghost
230 = storage.xyzh_ghost_layout->template get_field_idx<Tscal>(gsph::names::common::hpart);
231
232 // Set element counts
233 shambase::get_check_ref(storage.part_counts).indexes
234 = storage.merged_xyzh.get().template map<u32>(
235 [&](u64 id, shamrock::patch::PatchDataLayer &mpdat) {
236 return scheduler().patch_data.get_pdat(id).get_obj_cnt();
237 });
238
239 // Set element counts with ghost
240 shambase::get_check_ref(storage.part_counts_with_ghost).indexes
241 = storage.merged_xyzh.get().template map<u32>(
242 [&](u64 id, shamrock::patch::PatchDataLayer &mpdat) {
243 return mpdat.get_obj_cnt();
244 });
245
246 // Attach spans to block coords
247 shambase::get_check_ref(storage.positions_with_ghosts)
248 .set_refs(
249 storage.merged_xyzh.get().template map<std::reference_wrapper<PatchDataField<Tvec>>>(
250 [&, ixyz_ghost](u64 id, shamrock::patch::PatchDataLayer &mpdat) {
251 return std::ref(mpdat.get_field<Tvec>(ixyz_ghost));
252 }));
253
254 shambase::get_check_ref(storage.hpart_with_ghosts)
255 .set_refs(
256 storage.merged_xyzh.get().template map<std::reference_wrapper<PatchDataField<Tscal>>>(
257 [&, ihpart_ghost](u64 id, shamrock::patch::PatchDataLayer &mpdat) {
258 return std::ref(mpdat.get_field<Tscal>(ihpart_ghost));
259 }));
260}
261
262template<class Tvec, template<class> class Kern>
263void shammodels::gsph::Solver<Tvec, Kern>::build_merged_pos_trees() {
264 StackEntry stack_loc{};
265
266 auto &merged_xyzh = storage.merged_xyzh.get();
267 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
268
269 // Get field index from xyzh_ghost_layout
270 const u32 ixyz_ghost
271 = storage.xyzh_ghost_layout->template get_field_idx<Tvec>(gsph::names::common::xyz);
272
273 shambase::DistributedData<RTree> trees = merged_xyzh.template map<RTree>(
274 [&, ixyz_ghost](u64 id, shamrock::patch::PatchDataLayer &merged) {
275 PatchDataField<Tvec> &pos = merged.template get_field<Tvec>(ixyz_ghost);
276 Tvec bmax = pos.compute_max();
277 Tvec bmin = pos.compute_min();
278
279 shammath::AABB<Tvec> aabb(bmin, bmax);
280
281 Tscal infty = std::numeric_limits<Tscal>::infinity();
282
283 // Ensure that no particle is on the boundary of the AABB
284 aabb.lower[0] = std::nextafter(aabb.lower[0], -infty);
285 aabb.lower[1] = std::nextafter(aabb.lower[1], -infty);
286 aabb.lower[2] = std::nextafter(aabb.lower[2], -infty);
287 aabb.upper[0] = std::nextafter(aabb.upper[0], infty);
288 aabb.upper[1] = std::nextafter(aabb.upper[1], infty);
289 aabb.upper[2] = std::nextafter(aabb.upper[2], infty);
290
291 auto bvh = RTree::make_empty(dev_sched);
292 bvh.rebuild_from_positions(
293 pos.get_buf(), pos.get_obj_cnt(), aabb, solver_config.tree_reduction_level);
294
295 return bvh;
296 });
297
298 storage.merged_pos_trees.set(std::move(trees));
299}
300
301template<class Tvec, template<class> class Kern>
302void shammodels::gsph::Solver<Tvec, Kern>::clear_merged_pos_trees() {
303 StackEntry stack_loc{};
304 storage.merged_pos_trees.reset();
305}
306
307template<class Tvec, template<class> class Kern>
308void shammodels::gsph::Solver<Tvec, Kern>::compute_presteps_rint() {
309 StackEntry stack_loc{};
310
311 auto &xyzh_merged = storage.merged_xyzh.get();
312 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
313
314 // The Inutsuka V2 force formulation evaluates the kernel gradient at an
315 // effective smoothing length sqrt(2)*h (Inutsuka 2002). This tree-node
316 // interaction-range field is used to prune tree traversal in
317 // start_neighbors_cache(), so it must be widened by the same factor, or
318 // whole subtrees containing valid sqrt(2)*h-range neighbors get pruned
319 // before the leaf-level search even runs.
320 Tscal htol = solver_config.htol_up_coarse_cycle;
321 if (solver_config.is_force_inutsuka_v2()) {
322 htol *= shambase::constants::sqrt_2<Tscal>;
323 }
324
325 storage.rtree_rint_field.set(
326 storage.merged_pos_trees.get().template map<shamtree::KarrasRadixTreeField<Tscal>>(
327 [&](u64 id, RTree &rtree) -> shamtree::KarrasRadixTreeField<Tscal> {
328 shamrock::patch::PatchDataLayer &tmp = xyzh_merged.get(id);
329 auto &buf = tmp.get_field_buf_ref<Tscal>(1);
330 auto buf_int = shamtree::new_empty_karras_radix_tree_field<Tscal>();
331
332 auto ret = shamtree::compute_tree_field_max_field<Tscal>(
333 rtree.structure,
334 rtree.reduced_morton_set.get_leaf_cell_iterator(),
335 std::move(buf_int),
336 buf);
337
338 // Increase the size by tolerance factor
339 sham::kernel_call(
340 dev_sched->get_queue(),
341 sham::MultiRef{},
342 sham::MultiRef{ret.buf_field},
343 ret.buf_field.get_size(),
344 [htol](u32 i, Tscal *h_tree) {
345 h_tree[i] *= htol;
346 });
347
348 return std::move(ret);
349 }));
350}
351
352template<class Tvec, template<class> class Kern>
353void shammodels::gsph::Solver<Tvec, Kern>::reset_presteps_rint() {
354 storage.rtree_rint_field.reset();
355}
356
357template<class Tvec, template<class> class Kern>
358void shammodels::gsph::Solver<Tvec, Kern>::start_neighbors_cache() {
359 StackEntry stack_loc{};
360
361 shambase::Timer time_neigh;
362 time_neigh.start();
363
364 Tscal h_tolerance = solver_config.htol_up_coarse_cycle;
365
366 // The Inutsuka V2 force formulation evaluates the kernel gradient at an
367 // effective smoothing length sqrt(2)*h (Inutsuka 2002), so its support radius
368 // is sqrt(2) times larger than the standard h*Rkern cutoff used below. Widen
369 // the cached search radius accordingly, or pairs in (h*Rkern, sqrt(2)*h*Rkern)
370 // would silently be missing from the cache for that formulation.
371 if (solver_config.is_force_inutsuka_v2()) {
372 h_tolerance *= shambase::constants::sqrt_2<Tscal>;
373 }
374
375 // Build neighbor cache using tree traversal - same approach as SPH module
376 auto build_neigh_cache = [&](u64 patch_id) -> shamrock::tree::ObjectCache {
377 auto &mfield = storage.merged_xyzh.get().get(patch_id);
378
379 sham::DeviceBuffer<Tvec> &buf_xyz = mfield.template get_field_buf_ref<Tvec>(0);
380 sham::DeviceBuffer<Tscal> &buf_hpart = mfield.template get_field_buf_ref<Tscal>(1);
381
382 sham::DeviceBuffer<Tscal> &tree_field_rint
383 = storage.rtree_rint_field.get().get(patch_id).buf_field;
384
385 RTree &tree = storage.merged_pos_trees.get().get(patch_id);
386 auto obj_it = tree.get_object_iterator();
387
388 u32 obj_cnt = shambase::get_check_ref(storage.part_counts).indexes.get(patch_id);
389
390 constexpr Tscal Rker2 = Kernel::Rkern * Kernel::Rkern;
391
392 // Allocate neighbor count buffer
393 sham::DeviceBuffer<u32> neigh_count(
394 obj_cnt, shamsys::instance::get_compute_scheduler_ptr());
395
396 shamsys::instance::get_compute_queue().wait_and_throw();
397
398 // First pass: count neighbors
399 {
400 sham::DeviceQueue &q = shamsys::instance::get_compute_scheduler().get_queue();
401 sham::EventList depends_list;
402
403 auto xyz = buf_xyz.get_read_access(depends_list);
404 auto hpart = buf_hpart.get_read_access(depends_list);
405 auto rint_tree = tree_field_rint.get_read_access(depends_list);
406 auto neigh_cnt = neigh_count.get_write_access(depends_list);
407 auto particle_looper = obj_it.get_read_access(depends_list);
408
409 auto e = q.submit(depends_list, [&, h_tolerance](sycl::handler &cgh) {
410 shambase::parallel_for(cgh, obj_cnt, "gsph_count_neighbors", [=](u64 gid) {
411 u32 id_a = (u32) gid;
412
413 Tscal rint_a = hpart[id_a] * h_tolerance;
414 Tvec xyz_a = xyz[id_a];
415
416 Tvec inter_box_a_min = xyz_a - rint_a * Kernel::Rkern;
417 Tvec inter_box_a_max = xyz_a + rint_a * Kernel::Rkern;
418
419 u32 cnt = 0;
420
421 particle_looper.rtree_for(
422 [&](u32 node_id, shammath::AABB<Tvec> node_aabb) -> bool {
423 Tscal int_r_max_cell = rint_tree[node_id] * Kernel::Rkern;
424
425 using namespace walker::interaction_crit;
426
427 return sph_radix_cell_crit(
428 xyz_a,
429 inter_box_a_min,
430 inter_box_a_max,
431 node_aabb.lower,
432 node_aabb.upper,
433 int_r_max_cell);
434 },
435 [&](u32 id_b) {
436 Tvec dr = xyz_a - xyz[id_b];
437 Tscal rab2 = sycl::dot(dr, dr);
438 Tscal rint_b = hpart[id_b] * h_tolerance;
439
440 bool no_interact
441 = rab2 > rint_a * rint_a * Rker2 && rab2 > rint_b * rint_b * Rker2;
442
443 cnt += (no_interact) ? 0 : 1;
444 });
445
446 neigh_cnt[id_a] = cnt;
447 });
448 });
449
450 buf_xyz.complete_event_state(e);
451 buf_hpart.complete_event_state(e);
452 neigh_count.complete_event_state(e);
453 tree_field_rint.complete_event_state(e);
454 obj_it.complete_event_state(e);
455 }
456
457 // Use tree::prepare_object_cache to do prefix sum and allocate buffers
459 = shamrock::tree::prepare_object_cache(std::move(neigh_count), obj_cnt);
460
461 // Second pass: fill neighbor indices
462 {
463 sham::DeviceQueue &q = shamsys::instance::get_compute_scheduler().get_queue();
464 sham::EventList depends_list;
465
466 auto xyz = buf_xyz.get_read_access(depends_list);
467 auto hpart = buf_hpart.get_read_access(depends_list);
468 auto rint_tree = tree_field_rint.get_read_access(depends_list);
469 auto scanned_neigh_cnt = pcache.scanned_cnt.get_read_access(depends_list);
470 auto neigh = pcache.index_neigh_map.get_write_access(depends_list);
471 auto particle_looper = obj_it.get_read_access(depends_list);
472
473 auto e = q.submit(depends_list, [&, h_tolerance](sycl::handler &cgh) {
474 shambase::parallel_for(cgh, obj_cnt, "gsph_fill_neighbors", [=](u64 gid) {
475 u32 id_a = (u32) gid;
476
477 Tscal rint_a = hpart[id_a] * h_tolerance;
478 Tvec xyz_a = xyz[id_a];
479
480 Tvec inter_box_a_min = xyz_a - rint_a * Kernel::Rkern;
481 Tvec inter_box_a_max = xyz_a + rint_a * Kernel::Rkern;
482
483 u32 write_idx = scanned_neigh_cnt[id_a];
484
485 particle_looper.rtree_for(
486 [&](u32 node_id, shammath::AABB<Tvec> node_aabb) -> bool {
487 Tscal int_r_max_cell = rint_tree[node_id] * Kernel::Rkern;
488
489 using namespace walker::interaction_crit;
490
491 return sph_radix_cell_crit(
492 xyz_a,
493 inter_box_a_min,
494 inter_box_a_max,
495 node_aabb.lower,
496 node_aabb.upper,
497 int_r_max_cell);
498 },
499 [&](u32 id_b) {
500 Tvec dr = xyz_a - xyz[id_b];
501 Tscal rab2 = sycl::dot(dr, dr);
502 Tscal rint_b = hpart[id_b] * h_tolerance;
503
504 bool no_interact
505 = rab2 > rint_a * rint_a * Rker2 && rab2 > rint_b * rint_b * Rker2;
506
507 if (!no_interact) {
508 neigh[write_idx++] = id_b;
509 }
510 });
511 });
512 });
513
514 buf_xyz.complete_event_state(e);
515 buf_hpart.complete_event_state(e);
516 tree_field_rint.complete_event_state(e);
517 pcache.scanned_cnt.complete_event_state(e);
518 pcache.index_neigh_map.complete_event_state(e);
519 obj_it.complete_event_state(e);
520 }
521
522 return pcache;
523 };
524
525 shambase::get_check_ref(storage.neigh_cache).free_alloc();
526
527 using namespace shamrock::patch;
528 scheduler().for_each_patchdata_nonempty([&](Patch cur_p, PatchDataLayer &pdat) {
529 auto &ncache = shambase::get_check_ref(storage.neigh_cache);
530 ncache.neigh_cache.add_obj(cur_p.id_patch, build_neigh_cache(cur_p.id_patch));
531 });
532
533 time_neigh.stop();
534 storage.timings_details.neighbors += time_neigh.elapsed_sec();
535}
536
537template<class Tvec, template<class> class Kern>
538void shammodels::gsph::Solver<Tvec, Kern>::reset_neighbors_cache() {
539 storage.neigh_cache->neigh_cache = {};
540}
541
542template<class Tvec, template<class> class Kern>
544 StackEntry stack_loc{};
545
546 using namespace shamrock;
547 using namespace shamrock::patch;
548
549 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
550 const Tscal pmass = solver_config.gpart_mass;
551 static constexpr Tscal Rkern = Kernel::Rkern;
552
553 if (pmass == 0) {
555 "invalid gpart_mass 0 in compute_density, this configuration can not converge.");
556 }
557
558 shamrock::solvergraph::Field<Tscal> &density_field = shambase::get_check_ref(storage.density);
559
560 // Density is only needed for local particles here; it gets propagated to
561 // ghosts afterwards by communicate_merge_ghosts_fields().
562 shambase::DistributedData<u32> &counts = shambase::get_check_ref(storage.part_counts).indexes;
563 density_field.ensure_sizes(counts);
564
565 PatchDataLayerLayout &pdl = scheduler().pdl_old();
566 const u32 ihpart = pdl.get_field_idx<Tscal>(gsph::names::common::hpart);
567
568 auto &merged_xyzh = storage.merged_xyzh.get();
569 auto &neigh_cache = storage.neigh_cache->neigh_cache;
570
571 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
572 u32 cnt = pdat.get_obj_cnt();
573 if (cnt == 0)
574 return;
575
576 auto &mfield = merged_xyzh.get(p.id_patch);
577 auto &pcache = neigh_cache.get(p.id_patch);
578
579 // Position from merged data (includes ghosts for neighbor search)
580 auto &buf_xyz = mfield.template get_field_buf_ref<Tvec>(0);
581 auto &buf_hpart = pdat.get_field_buf_ref<Tscal>(ihpart);
582
583 auto &dens_field = density_field.get_field(p.id_patch);
584
585 sham::DeviceQueue &q = dev_sched->get_queue();
586 sham::EventList depends_list;
587
588 auto ploop_ptrs = pcache.get_read_access(depends_list);
589 auto xyz_acc = buf_xyz.get_read_access(depends_list);
590 auto h_acc = buf_hpart.get_read_access(depends_list);
591 auto density_acc = dens_field.get_buf().get_write_access(depends_list);
592
593 auto e = q.submit(depends_list, [&, pmass](sycl::handler &cgh) {
594 shamrock::tree::ObjectCacheIterator particle_looper(ploop_ptrs);
595
596 shambase::parallel_for(cgh, cnt, "gsph_compute_density", [=](u64 gid) {
597 u32 id_a = (u32) gid;
598
599 Tvec xyz_a = xyz_acc[id_a];
600 Tscal h_a = h_acc[id_a];
601 Tscal dint = h_a * h_a * Rkern * Rkern;
602
603 // SPH density summation: rho_a = sum_b m_b W(|r_a - r_b|, h_a)
604 // (includes the self contribution b == a)
605 Tscal rho_sum = pmass * Kernel::W_3d(Tscal(0), h_a);
606
607 particle_looper.for_each_object(id_a, [&](u32 id_b) {
608 if (id_b == id_a) {
609 return;
610 }
611
612 Tvec dr = xyz_a - xyz_acc[id_b];
613 Tscal rab2 = sycl::dot(dr, dr);
614
615 if (rab2 > dint) {
616 return;
617 }
618
619 Tscal rab = sycl::sqrt(rab2);
620 rho_sum += pmass * Kernel::W_3d(rab, h_a);
621 });
622
623 density_acc[id_a] = sycl::max(rho_sum, Tscal(1e-30));
624 });
625 });
626
627 pcache.complete_event_state({e});
628 buf_xyz.complete_event_state(e);
629 buf_hpart.complete_event_state(e);
630 dens_field.get_buf().complete_event_state(e);
631 });
632}
633
634template<class Tvec, template<class> class Kern>
635void shammodels::gsph::Solver<Tvec, Kern>::gsph_prestep(Tscal time_val, Tscal dt) {
636 StackEntry stack_loc{};
637
638 shamlog_debug_ln("GSPH", "Prestep at t =", time_val, "dt =", dt);
639
640 using namespace shamrock;
641 using namespace shamrock::patch;
642
644 using SPHUtils = gsph::GSPHUtilities<Tvec, Kernel>;
645
646 SPHUtils sph_utils(scheduler());
647 shamrock::SchedulerUtility utility(scheduler());
648
649 PatchDataLayerLayout &pdl = scheduler().pdl_old();
650 const u32 ihpart = pdl.get_field_idx<Tscal>("hpart");
651
652 ComputeField<Tscal> _epsilon_h, _h_old;
653
654 auto should_set_omega_mask = std::make_shared<shamrock::solvergraph::Field<u32>>(
655 1, "should_set_omega_mask", "should_set_omega_mask");
656
657 u32 hstep_cnt = 0;
658 u32 hstep_max = solver_config.h_max_subcycles_count;
659 for (; hstep_cnt < hstep_max; hstep_cnt++) {
660
661 gen_ghost_handler(time_val + dt);
662 build_ghost_cache();
663 merge_position_ghost();
664 build_merged_pos_trees();
665 compute_presteps_rint();
666 start_neighbors_cache();
667
668 _epsilon_h = utility.make_compute_field<Tscal>("epsilon_h", 1, Tscal(100));
669 _h_old = utility.save_field<Tscal>(ihpart, "h_old");
670
671 Tscal max_eps_h;
672
673 if (solver_config.gpart_mass == 0) {
675 "invalid gpart_mass {}, this configuration can not converge.\n"
676 "Please set it using either model.set_particle_mass(pmass) or "
677 "cfg.set_particle_mass(pmass)",
678 solver_config.gpart_mass));
679 }
680
681 // sizes
682 std::shared_ptr<shamrock::solvergraph::Indexes<u32>> sizes
683 = std::make_shared<shamrock::solvergraph::Indexes<u32>>("", "");
684 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
685 sizes->indexes.add_obj(p.id_patch, pdat.get_obj_cnt());
686 });
687
688 // neigh cache
689 auto &neigh_cache = storage.neigh_cache;
690
691 // positions
692 auto &pos_merged = storage.positions_with_ghosts;
693
694 // old smoothing length field
695 std::shared_ptr<shamrock::solvergraph::FieldRefs<Tscal>> hold
696 = std::make_shared<shamrock::solvergraph::FieldRefs<Tscal>>("", "");
698 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
699 auto &field = _h_old.get_field(p.id_patch);
700 hold_refs.add_obj(p.id_patch, std::ref(field));
701 });
702 hold->set_refs(hold_refs);
703
704 // new smoothing length field
705 std::shared_ptr<shamrock::solvergraph::FieldRefs<Tscal>> hnew
706 = std::make_shared<shamrock::solvergraph::FieldRefs<Tscal>>("", "");
708 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
709 auto &field = pdat.get_field<Tscal>(ihpart);
710 hnew_refs.add_obj(p.id_patch, std::ref(field));
711 });
712 hnew->set_refs(hnew_refs);
713
714 // epsilon field
715 std::shared_ptr<shamrock::solvergraph::FieldRefs<Tscal>> eps_h
716 = std::make_shared<shamrock::solvergraph::FieldRefs<Tscal>>("", "");
718 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
719 auto &field = _epsilon_h.get_field(p.id_patch);
720 eps_h_refs.add_obj(p.id_patch, std::ref(field));
721 });
722 eps_h->set_refs(eps_h_refs);
723
724 std::shared_ptr<shamrock::solvergraph::INode> smth_h_iter_ptr;
725
726 using h_conf_density_based = typename gsph::SmoothingLengthConfig::DensityBased;
727 using h_conf_neigh_lim = typename gsph::SmoothingLengthConfig::DensityBasedNeighLim;
728
729 if (h_conf_density_based *conf
730 = std::get_if<h_conf_density_based>(&solver_config.smoothing_length_config.config)) {
731 std::shared_ptr<shammodels::sph::modules::IterateSmoothingLengthDensity<Tvec, Kernel>>
732 smth_h_iter = std::make_shared<
734 solver_config.gpart_mass,
735 solver_config.htol_up_coarse_cycle,
736 solver_config.htol_up_fine_cycle,
737 solver_config.epsilon_h);
738 smth_h_iter->set_edges(sizes, neigh_cache, pos_merged, hold, hnew, eps_h);
739 smth_h_iter_ptr = smth_h_iter;
740 } else if (
741 h_conf_neigh_lim *conf
742 = std::get_if<h_conf_neigh_lim>(&solver_config.smoothing_length_config.config)) {
743 std::shared_ptr<
745 smth_h_iter_neigh_lim = std::make_shared<
747 solver_config.gpart_mass,
748 solver_config.htol_up_coarse_cycle,
749 solver_config.htol_up_fine_cycle,
750 conf->max_neigh_count,
751 solver_config.epsilon_h);
752 smth_h_iter_neigh_lim->set_edges(
753 sizes, neigh_cache, pos_merged, hold, hnew, eps_h, should_set_omega_mask);
754 smth_h_iter_ptr = smth_h_iter_neigh_lim;
755 } else {
756 shambase::throw_with_loc<std::runtime_error>("Invalid smoothing length configuration");
757 }
758 // iterate smoothing length
759
760 std::shared_ptr<shamrock::solvergraph::IDataEdge<bool>> is_converged
761 = shamrock::solvergraph::IDataEdge<bool>::make_shared("", "");
762
764 smth_h_iter_ptr, solver_config.epsilon_h, solver_config.h_iter_per_subcycles, false);
765 loop_smth_h_iter.set_edges(eps_h, is_converged);
766
767 loop_smth_h_iter.evaluate();
768
769 if (!is_converged->data) {
770
771 Tscal largest_h = 0;
772
773 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
774 largest_h = sham::max(largest_h, pdat.get_field<Tscal>(ihpart).compute_max());
775 });
776 Tscal global_largest_h = shamalgs::collective::allreduce_max(largest_h);
777
778 std::string add_info = "";
779 u64 cnt_unconverged = 0;
780 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
781 auto res
782 = _epsilon_h.get_field(p.id_patch).get_ids_buf_where([](auto access, u32 id) {
783 return access[id] == -1;
784 });
785
786 if (hstep_cnt == hstep_max - 1) {
787 if (std::get<0>(res)) {
788 add_info += "\n patch " + std::to_string(p.id_patch) + " ";
789 add_info += "errored parts : \n";
790 sycl::buffer<u32> &idx_err = *std::get<0>(res);
791
792 sham::DeviceBuffer<Tvec> &xyz = pdat.get_field_buf_ref<Tvec>(0);
793 sham::DeviceBuffer<Tscal> &hpart = pdat.get_field_buf_ref<Tscal>(ihpart);
794
795 auto pos = xyz.copy_to_stdvec();
796 auto h = hpart.copy_to_stdvec();
797
798 {
799 sycl::host_accessor acc{idx_err};
800 for (u32 i = 0; i < idx_err.size(); i++) {
801 add_info += sham::format(
802 "{} - pos : {}, hpart : {}\n", acc[i], pos[acc[i]], h[acc[i]]);
803 }
804 }
805 }
806 }
807
808 cnt_unconverged += std::get<1>(res);
809 });
810
811 u64 global_cnt_unconverged = shamalgs::collective::allreduce_sum(cnt_unconverged);
812
813 if (shamcomm::world_rank() == 0) {
815 "Smoothinglength",
816 "smoothing length is not converged, rerunning the iterator ...\n largest h "
817 "=",
818 global_largest_h,
819 "unconverged cnt =",
820 global_cnt_unconverged,
821 add_info);
822 }
823
824 reset_ghost_handler();
825 clear_ghost_cache();
826
827 shambase::get_check_ref(storage.part_counts).free_alloc();
828 shambase::get_check_ref(storage.part_counts_with_ghost).free_alloc();
829 shambase::get_check_ref(storage.positions_with_ghosts).free_alloc();
830 shambase::get_check_ref(storage.hpart_with_ghosts).free_alloc();
831
832 storage.merged_xyzh.reset();
833
834 clear_merged_pos_trees();
835 reset_presteps_rint();
836 reset_neighbors_cache();
837
838 // scheduler().for_each_patchdata_nonempty([&](Patch cur_p, PatchData &pdat) {
839 // pdat.synchronize_buf();
840 // });
841
842 continue;
843 }
844
845 // The hpart is not valid anymore in ghost zones since we iterated it's value
846 shambase::get_check_ref(storage.hpart_with_ghosts).free_alloc();
847
848 _epsilon_h.reset();
849 _h_old.reset();
850 break;
851 }
852
853 if (hstep_cnt == hstep_max) {
854 logger::err_ln("GSPH", "the h iterator is not converged after", hstep_cnt, "iterations");
855 }
856
857 std::shared_ptr<shamrock::solvergraph::FieldRefs<Tscal>> hnew_edge
858 = std::make_shared<shamrock::solvergraph::FieldRefs<Tscal>>("", "");
860 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
861 auto &field = pdat.get_field<Tscal>(ihpart);
862 hnew_refs.add_obj(p.id_patch, std::ref(field));
863 });
864 hnew_edge->set_refs(hnew_refs);
865
866 sph::modules::NodeComputeOmega<Tvec, Kern> compute_omega{solver_config.gpart_mass};
867 compute_omega.set_edges(
868 storage.part_counts,
869 storage.neigh_cache,
870 storage.positions_with_ghosts,
871 hnew_edge,
872 storage.omega);
873 compute_omega.evaluate();
874
875 if (solver_config.smoothing_length_config.is_density_based_neigh_lim()) {
876 // if the h limiter is triggered, omega does not hold it's sense of dh/dr anymore
877 // so we set it to 1, this effectively is equivalent of disabling the energy correction
878 // term corresponding to dh/dr
879 sph::modules::SetWhenMask<Tscal> set_omega_mask{1};
880 set_omega_mask.set_edges(storage.part_counts, should_set_omega_mask, storage.omega);
881 set_omega_mask.evaluate();
882 }
883
884 // NodeComputeOmega only produces the grad-h correction factor, not density
885 // itself (see Solver.hpp::compute_density() for why GSPH needs an explicit
886 // summed density field, unlike plain SPH). Compute it now, on the same
887 // converged h / neighbor cache / merged positions used above.
888 compute_density();
889}
890
891template<class Tvec, template<class> class Kern>
892void shammodels::gsph::Solver<Tvec, Kern>::apply_position_boundary(Tscal time_val) {
893 StackEntry stack_loc{};
894
895 shamlog_debug_ln("GSPH", "apply position boundary");
896
897 PatchScheduler &sched = scheduler();
898 shamrock::SchedulerUtility integrators(sched);
900
901 auto &pdl = sched.pdl_old();
902 const u32 ixyz = pdl.get_field_idx<Tvec>(gsph::names::common::xyz);
903 const u32 ivxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::vxyz);
904 auto [bmin, bmax] = sched.get_box_volume<Tvec>();
905
906 using SolverConfigBC = typename Config::BCConfig;
907 using SolverBCFree = typename SolverConfigBC::Free;
908 using SolverBCPeriodic = typename SolverConfigBC::Periodic;
909 using SolverBCShearingPeriodic = typename SolverConfigBC::ShearingPeriodic;
910
911 if (SolverBCFree *c = std::get_if<SolverBCFree>(&solver_config.boundary_config.config)) {
912 if (shamcomm::world_rank() == 0) {
913 logger::info_ln("PositionUpdated", "free boundaries skipping geometry update");
914 }
915 } else if (
916 SolverBCPeriodic *c
917 = std::get_if<SolverBCPeriodic>(&solver_config.boundary_config.config)) {
918 integrators.fields_apply_periodicity(ixyz, std::pair{bmin, bmax});
919 } else if (
920 SolverBCShearingPeriodic *c
921 = std::get_if<SolverBCShearingPeriodic>(&solver_config.boundary_config.config)) {
922 // Apply shearing periodic boundaries (Stone 2010) - reuse SPH implementation
923 integrators.fields_apply_shearing_periodicity(
924 ixyz,
925 ivxyz,
926 std::pair{bmin, bmax},
927 c->shear_base,
928 c->shear_dir,
929 c->shear_speed * time_val,
930 c->shear_speed);
931 } else {
932 shambase::throw_with_loc<std::runtime_error>("GSPH: Unsupported boundary condition type.");
933 }
934
935 reatrib.reatribute_patch_objects(storage.serial_patch_tree.get(), gsph::names::common::xyz);
936}
937
938template<class Tvec, template<class> class Kern>
939void shammodels::gsph::Solver<Tvec, Kern>::do_predictor_leapfrog(Tscal dt) {
940 StackEntry stack_loc{};
941 using namespace shamrock::patch;
942
943 PatchDataLayerLayout &pdl = scheduler().pdl_old();
944 const u32 ixyz = pdl.get_field_idx<Tvec>(gsph::names::common::xyz);
945 const u32 ivxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::vxyz);
946 const u32 iaxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::axyz);
947
948 const bool has_uint = solver_config.has_field_uint();
949 const u32 iuint = has_uint ? pdl.get_field_idx<Tscal>(gsph::names::newtonian::uint) : 0;
950 const u32 iduint = has_uint ? pdl.get_field_idx<Tscal>(gsph::names::newtonian::duint) : 0;
951
952 Tscal half_dt = dt / 2;
953
954 // Predictor step: leapfrog kick-drift-kick
955 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
956 u32 cnt = pdat.get_obj_cnt();
957 if (cnt == 0)
958 return;
959
960 auto &xyz_field = pdat.get_field<Tvec>(ixyz);
961 auto &vxyz_field = pdat.get_field<Tvec>(ivxyz);
962 auto &axyz_field = pdat.get_field<Tvec>(iaxyz);
963
964 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
965
966 // Leapfrog KDK: first half-kick, then drift
967 // The second half-kick (corrector) happens AFTER force recomputation
969 dev_sched->get_queue(),
970 sham::MultiRef{axyz_field.get_buf()},
971 sham::MultiRef{xyz_field.get_buf(), vxyz_field.get_buf()},
972 cnt,
973 [half_dt, dt](u32 i, const Tvec *axyz, Tvec *xyz, Tvec *vxyz) {
974 // First kick: v += a*dt/2 (using OLD acceleration)
975 vxyz[i] += axyz[i] * half_dt;
976 // Drift: x += v*dt
977 xyz[i] += vxyz[i] * dt;
978 });
979
980 // Internal energy integration (if adiabatic EOS)
981 // Predictor: u += du*dt/2 (first half-step)
982 // The second half-step happens in the corrector after force recomputation
983 if (has_uint) {
984 auto &uint_field = pdat.get_field<Tscal>(iuint);
985 auto &duint_field = pdat.get_field<Tscal>(iduint);
986
988 dev_sched->get_queue(),
989 sham::MultiRef{duint_field.get_buf()},
990 sham::MultiRef{uint_field.get_buf()},
991 cnt,
992 [half_dt](u32 i, const Tscal *duint, Tscal *uint) {
993 // u += du*dt/2 (first half-step)
994 uint[i] += duint[i] * half_dt;
995 });
996 }
997 });
998}
999
1000template<class Tvec, template<class> class Kern>
1001void shammodels::gsph::Solver<Tvec, Kern>::init_ghost_layout() {
1002 StackEntry stack_loc{};
1003
1004 // Initialize xyzh_ghost_layout for BasicSPHGhostHandler (position + smoothing length)
1005 storage.xyzh_ghost_layout = std::make_shared<shamrock::patch::PatchDataLayerLayout>();
1006 storage.xyzh_ghost_layout->template add_field<Tvec>(gsph::names::common::xyz, 1);
1007 storage.xyzh_ghost_layout->template add_field<Tscal>(gsph::names::common::hpart, 1);
1008
1009 // Reset first in case it was set from a previous timestep
1010 storage.ghost_layout = std::make_shared<shamrock::patch::PatchDataLayerLayout>();
1011
1013 = shambase::get_check_ref(storage.ghost_layout.get());
1014
1015 solver_config.set_ghost_layout(ghost_layout);
1016}
1017
1018template<class Tvec, template<class> class Kern>
1019void shammodels::gsph::Solver<Tvec, Kern>::communicate_merge_ghosts_fields() {
1020 StackEntry stack_loc{};
1021
1022 shambase::Timer timer_interf;
1023 timer_interf.start();
1024
1025 using namespace shamrock;
1026 using namespace shamrock::patch;
1027
1028 PatchDataLayerLayout &pdl = scheduler().pdl_old();
1029 const u32 ixyz = pdl.get_field_idx<Tvec>(gsph::names::common::xyz);
1030 const u32 ivxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::vxyz);
1031 const u32 ihpart = pdl.get_field_idx<Tscal>(gsph::names::common::hpart);
1032
1033 const bool has_uint = solver_config.has_field_uint();
1034 const u32 iuint = has_uint ? pdl.get_field_idx<Tscal>(gsph::names::newtonian::uint) : 0;
1035
1036 auto &ghost_layout_ptr = storage.ghost_layout;
1037 shamrock::patch::PatchDataLayerLayout &ghost_layout = shambase::get_check_ref(ghost_layout_ptr);
1038 u32 ihpart_interf = ghost_layout.get_field_idx<Tscal>(gsph::names::common::hpart);
1039 u32 ivxyz_interf = ghost_layout.get_field_idx<Tvec>(gsph::names::newtonian::vxyz);
1040 u32 iomega_interf = ghost_layout.get_field_idx<Tscal>(gsph::names::newtonian::omega);
1041 u32 idensity_interf = ghost_layout.get_field_idx<Tscal>(gsph::names::newtonian::density);
1042 u32 iuint_interf
1043 = has_uint ? ghost_layout.get_field_idx<Tscal>(gsph::names::newtonian::uint) : 0;
1044
1045 // Gradient field indices (for MUSCL reconstruction)
1046 const bool has_grads = solver_config.requires_gradients();
1047 u32 igrad_d_interf
1048 = has_grads ? ghost_layout.get_field_idx<Tvec>(gsph::names::newtonian::grad_density) : 0;
1049 u32 igrad_p_interf
1050 = has_grads ? ghost_layout.get_field_idx<Tvec>(gsph::names::newtonian::grad_pressure) : 0;
1051 u32 igrad_vx_interf
1052 = has_grads ? ghost_layout.get_field_idx<Tvec>(gsph::names::newtonian::grad_vx) : 0;
1053 u32 igrad_vy_interf
1054 = has_grads ? ghost_layout.get_field_idx<Tvec>(gsph::names::newtonian::grad_vy) : 0;
1055 u32 igrad_vz_interf
1056 = has_grads ? ghost_layout.get_field_idx<Tvec>(gsph::names::newtonian::grad_vz) : 0;
1057
1058 using InterfaceBuildInfos = typename gsph::GSPHGhostHandler<Tvec>::InterfaceBuildInfos;
1059
1060 gsph::GSPHGhostHandler<Tvec> &ghost_handle
1061 = shambase::get_check_ref(storage.ghost_handler).get();
1064
1065 // Get gradient fields (for MUSCL)
1066 shamrock::solvergraph::Field<Tvec> *grad_density_ptr
1067 = has_grads ? &shambase::get_check_ref(storage.grad_density) : nullptr;
1068 shamrock::solvergraph::Field<Tvec> *grad_pressure_ptr
1069 = has_grads ? &shambase::get_check_ref(storage.grad_pressure) : nullptr;
1071 = has_grads ? &shambase::get_check_ref(storage.grad_vx) : nullptr;
1073 = has_grads ? &shambase::get_check_ref(storage.grad_vy) : nullptr;
1075 = has_grads ? &shambase::get_check_ref(storage.grad_vz) : nullptr;
1076
1077 // Build interface data from ghost cache
1078 auto pdat_interf = ghost_handle.template build_interface_native<PatchDataLayer>(
1079 storage.ghost_patch_cache.get(),
1080 [&](u64 sender, u64, InterfaceBuildInfos binfo, sham::DeviceBuffer<u32> &buf_idx, u32 cnt) {
1081 PatchDataLayer pdat(ghost_layout_ptr);
1082 pdat.reserve(cnt);
1083 return pdat;
1084 });
1085
1086 // Populate interface data with field values
1087 ghost_handle.template modify_interface_native<PatchDataLayer>(
1088 storage.ghost_patch_cache.get(),
1089 pdat_interf,
1090 [&](u64 sender,
1091 u64,
1092 InterfaceBuildInfos binfo,
1093 sham::DeviceBuffer<u32> &buf_idx,
1094 u32 cnt,
1095 PatchDataLayer &pdat) {
1096 PatchDataLayer &sender_patch = scheduler().patch_data.get_pdat(sender);
1097 PatchDataField<Tscal> &sender_omega = omega.get(sender);
1098 PatchDataField<Tscal> &sender_density = density.get(sender);
1099
1100 sender_patch.get_field<Tscal>(ihpart).append_subset_to(
1101 buf_idx, cnt, pdat.get_field<Tscal>(ihpart_interf));
1102 sender_patch.get_field<Tvec>(ivxyz).append_subset_to(
1103 buf_idx, cnt, pdat.get_field<Tvec>(ivxyz_interf));
1104 sender_omega.append_subset_to(buf_idx, cnt, pdat.get_field<Tscal>(iomega_interf));
1105 sender_density.append_subset_to(buf_idx, cnt, pdat.get_field<Tscal>(idensity_interf));
1106
1107 if (has_uint) {
1108 sender_patch.get_field<Tscal>(iuint).append_subset_to(
1109 buf_idx, cnt, pdat.get_field<Tscal>(iuint_interf));
1110 }
1111
1112 // Communicate gradient fields for MUSCL reconstruction
1113 if (has_grads) {
1114 grad_density_ptr->get(sender).append_subset_to(
1115 buf_idx, cnt, pdat.get_field<Tvec>(igrad_d_interf));
1116 grad_pressure_ptr->get(sender).append_subset_to(
1117 buf_idx, cnt, pdat.get_field<Tvec>(igrad_p_interf));
1118 grad_vx_ptr->get(sender).append_subset_to(
1119 buf_idx, cnt, pdat.get_field<Tvec>(igrad_vx_interf));
1120 grad_vy_ptr->get(sender).append_subset_to(
1121 buf_idx, cnt, pdat.get_field<Tvec>(igrad_vy_interf));
1122 grad_vz_ptr->get(sender).append_subset_to(
1123 buf_idx, cnt, pdat.get_field<Tvec>(igrad_vz_interf));
1124 }
1125 });
1126
1127 // Apply velocity offset for periodic boundaries
1128 ghost_handle.template modify_interface_native<PatchDataLayer>(
1129 storage.ghost_patch_cache.get(),
1130 pdat_interf,
1131 [&](u64 sender,
1132 u64,
1133 InterfaceBuildInfos binfo,
1134 sham::DeviceBuffer<u32> &buf_idx,
1135 u32 cnt,
1136 PatchDataLayer &pdat) {
1137 if (sycl::length(binfo.offset_speed) > 0) {
1138 pdat.get_field<Tvec>(ivxyz_interf).apply_offset(binfo.offset_speed);
1139 }
1140 });
1141
1142 // Communicate ghost data across MPI ranks
1144 = ghost_handle.communicate_pdat(ghost_layout_ptr, std::move(pdat_interf));
1145
1146 // Count total ghost particles per patch
1147 std::map<u64, u64> sz_interf_map;
1148 interf_pdat.for_each([&](u64 s, u64 r, PatchDataLayer &pdat_interf) {
1149 sz_interf_map[r] += pdat_interf.get_obj_cnt();
1150 });
1151
1152 // Merge local and ghost data
1153 storage.merged_patchdata_ghost.set(
1154 ghost_handle.template merge_native<PatchDataLayer, PatchDataLayer>(
1155 std::move(interf_pdat),
1157 PatchDataLayer pdat_new(ghost_layout_ptr);
1158
1159 u32 or_elem = pdat.get_obj_cnt();
1160 pdat_new.reserve(or_elem + sz_interf_map[p.id_patch]);
1161
1162 PatchDataField<Tscal> &cur_omega = omega.get(p.id_patch);
1163 PatchDataField<Tscal> &cur_density = density.get(p.id_patch);
1164
1165 // Insert local particle data
1166 pdat_new.get_field<Tscal>(ihpart_interf).insert(pdat.get_field<Tscal>(ihpart));
1167 pdat_new.get_field<Tvec>(ivxyz_interf).insert(pdat.get_field<Tvec>(ivxyz));
1168 pdat_new.get_field<Tscal>(iomega_interf).insert(cur_omega);
1169 pdat_new.get_field<Tscal>(idensity_interf).insert(cur_density);
1170
1171 if (has_uint) {
1172 pdat_new.get_field<Tscal>(iuint_interf).insert(pdat.get_field<Tscal>(iuint));
1173 }
1174
1175 // Insert local gradient data for MUSCL reconstruction
1176 if (has_grads) {
1177 pdat_new.get_field<Tvec>(igrad_d_interf)
1178 .insert(grad_density_ptr->get(p.id_patch));
1179 pdat_new.get_field<Tvec>(igrad_p_interf)
1180 .insert(grad_pressure_ptr->get(p.id_patch));
1181 pdat_new.get_field<Tvec>(igrad_vx_interf).insert(grad_vx_ptr->get(p.id_patch));
1182 pdat_new.get_field<Tvec>(igrad_vy_interf).insert(grad_vy_ptr->get(p.id_patch));
1183 pdat_new.get_field<Tvec>(igrad_vz_interf).insert(grad_vz_ptr->get(p.id_patch));
1184 }
1185
1186 pdat_new.check_field_obj_cnt_match();
1187 return pdat_new;
1188 },
1189 [](PatchDataLayer &pdat, PatchDataLayer &pdat_interf) {
1190 pdat.insert_elements(pdat_interf);
1191 }));
1192
1193 timer_interf.stop();
1194 storage.timings_details.interface += timer_interf.elapsed_sec();
1195}
1196
1197template<class Tvec, template<class> class Kern>
1198void shammodels::gsph::Solver<Tvec, Kern>::reset_merge_ghosts_fields() {
1199 storage.merged_patchdata_ghost.reset();
1200}
1201
1202template<class Tvec, template<class> class Kern>
1203void shammodels::gsph::Solver<Tvec, Kern>::compute_eos_fields() {
1204 StackEntry stack_loc{};
1205
1206 using namespace shamrock;
1207 using namespace shamrock::patch;
1208
1209 // GSPH EOS: Following reference implementation (g_pre_interaction.cpp)
1210 // P = (\gamma - 1) * \rho * u where \rho is from SPH summation
1211 // c = sqrt(\gamma * (\gamma - 1) * u) -- from internal energy, not from P/\rho
1212
1213 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
1214 const Tscal gamma = solver_config.get_eos_gamma();
1215 const bool has_uint = solver_config.has_field_uint();
1216
1217 // Get ghost layout field indices
1219 = shambase::get_check_ref(storage.ghost_layout.get());
1220 u32 idensity_interf = ghost_layout.get_field_idx<Tscal>(gsph::names::newtonian::density);
1221 u32 iuint_interf
1222 = has_uint ? ghost_layout.get_field_idx<Tscal>(gsph::names::newtonian::uint) : 0;
1223
1224 shamrock::solvergraph::Field<Tscal> &pressure_field = shambase::get_check_ref(storage.pressure);
1225 shamrock::solvergraph::Field<Tscal> &soundspeed_field
1226 = shambase::get_check_ref(storage.soundspeed);
1227
1228 // Size buffers to part_counts_with_ghost (includes ghosts!)
1229 shambase::DistributedData<u32> &counts_with_ghosts
1230 = shambase::get_check_ref(storage.part_counts_with_ghost).indexes;
1231
1232 pressure_field.ensure_sizes(counts_with_ghosts);
1233 soundspeed_field.ensure_sizes(counts_with_ghosts);
1234
1235 // Iterate over merged_patchdata_ghost (includes local + ghost particles)
1236 storage.merged_patchdata_ghost.get().for_each([&](u64 id, PatchDataLayer &mpdat) {
1237 u32 total_elements
1238 = shambase::get_check_ref(storage.part_counts_with_ghost).indexes.get(id);
1239 if (total_elements == 0)
1240 return;
1241
1242 // Use SPH-summation density from communicated ghost data
1243 sham::DeviceBuffer<Tscal> &buf_density = mpdat.get_field_buf_ref<Tscal>(idensity_interf);
1244 auto &pressure_buf = pressure_field.get_field(id).get_buf();
1245 auto &soundspeed_buf = soundspeed_field.get_field(id).get_buf();
1246
1247 sham::DeviceQueue &q = dev_sched->get_queue();
1248 sham::EventList depends_list;
1249
1250 auto density = buf_density.get_read_access(depends_list);
1251 auto pressure = pressure_buf.get_write_access(depends_list);
1252 auto soundspeed = soundspeed_buf.get_write_access(depends_list);
1253
1254 const Tscal *uint_ptr = nullptr;
1255 if (has_uint) {
1256 uint_ptr = mpdat.get_field_buf_ref<Tscal>(iuint_interf).get_read_access(depends_list);
1257 }
1258
1259 auto e = q.submit(depends_list, [&](sycl::handler &cgh) {
1260 shambase::parallel_for(cgh, total_elements, "compute_eos_gsph", [=](u64 gid) {
1261 u32 i = (u32) gid;
1262
1263 // Use SPH-summation density (from compute_omega, communicated to ghosts)
1264 Tscal rho = density[i];
1265 rho = sycl::max(rho, Tscal(1e-30));
1266
1267 if (has_uint && uint_ptr != nullptr) {
1268 // Adiabatic EOS (reference: g_pre_interaction.cpp line 107)
1269 // P = (\gamma - 1) * \rho * u
1270 Tscal u = uint_ptr[i];
1271 u = sycl::max(u, Tscal(1e-30));
1272 Tscal P = (gamma - Tscal(1.0)) * rho * u;
1273
1274 // Sound speed from internal energy (reference: solver.cpp line 2661)
1275 // c = sqrt(\gamma * (\gamma - 1) * u)
1276 Tscal cs = sycl::sqrt(gamma * (gamma - Tscal(1.0)) * u);
1277
1278 // Clamp to reasonable values
1279 P = sycl::clamp(P, Tscal(1e-30), Tscal(1e30));
1280 cs = sycl::clamp(cs, Tscal(1e-10), Tscal(1e10));
1281
1282 pressure[i] = P;
1283 soundspeed[i] = cs;
1284 } else {
1285 // Isothermal case
1286 Tscal cs = Tscal(1.0);
1287 Tscal P = cs * cs * rho;
1288
1289 pressure[i] = P;
1290 soundspeed[i] = cs;
1291 }
1292 });
1293 });
1294
1295 // Complete all buffer event states
1296 buf_density.complete_event_state(e);
1297 if (has_uint) {
1298 mpdat.get_field_buf_ref<Tscal>(iuint_interf).complete_event_state(e);
1299 }
1300 pressure_buf.complete_event_state(e);
1301 soundspeed_buf.complete_event_state(e);
1302 });
1303}
1304
1305template<class Tvec, template<class> class Kern>
1306void shammodels::gsph::Solver<Tvec, Kern>::reset_eos_fields() {
1307 // Reset computed EOS fields - they're recomputed each timestep
1308}
1309
1310template<class Tvec, template<class> class Kern>
1312 StackEntry stack_loc{};
1313
1314 using namespace shamrock;
1315 using namespace shamrock::patch;
1316
1317 // Copy density, pressure, and soundspeed from solvergraph fields to patchdata
1318 // This ensures the values persist across restarts and can be read by VTKDump
1319
1320 PatchDataLayerLayout &pdl = scheduler().pdl_old();
1321 u32 idensity = pdl.get_field_idx<Tscal>(names::newtonian::density);
1322 u32 ipressure = pdl.get_field_idx<Tscal>(names::newtonian::pressure);
1323 u32 isoundspeed = pdl.get_field_idx<Tscal>(names::newtonian::soundspeed);
1324
1325 auto &density_field = shambase::get_check_ref(storage.density);
1326 auto &pressure_field = shambase::get_check_ref(storage.pressure);
1327 auto &soundspeed_field = shambase::get_check_ref(storage.soundspeed);
1328
1329 scheduler().for_each_patchdata_nonempty([&](Patch cur_p, PatchDataLayer &pdat) {
1330 u32 npart = pdat.get_obj_cnt();
1331 if (npart == 0) {
1332 return;
1333 }
1334
1335 // Get patchdata buffers
1336 sham::DeviceBuffer<Tscal> &buf_rho = pdat.get_field_buf_ref<Tscal>(idensity);
1337 sham::DeviceBuffer<Tscal> &buf_P = pdat.get_field_buf_ref<Tscal>(ipressure);
1338 sham::DeviceBuffer<Tscal> &buf_cs = pdat.get_field_buf_ref<Tscal>(isoundspeed);
1339
1340 // Get solvergraph field buffers (source data)
1341 sham::DeviceBuffer<Tscal> &buf_rho_in = density_field.get_field(cur_p.id_patch).get_buf();
1342 sham::DeviceBuffer<Tscal> &buf_P_in = pressure_field.get_field(cur_p.id_patch).get_buf();
1343 sham::DeviceBuffer<Tscal> &buf_cs_in = soundspeed_field.get_field(cur_p.id_patch).get_buf();
1344
1345 auto &q = shamsys::instance::get_compute_scheduler().get_queue();
1346 sham::EventList depends_list;
1347
1348 auto rho_in = buf_rho_in.get_read_access(depends_list);
1349 auto P_in = buf_P_in.get_read_access(depends_list);
1350 auto cs_in = buf_cs_in.get_read_access(depends_list);
1351 auto rho = buf_rho.get_write_access(depends_list);
1352 auto P = buf_P.get_write_access(depends_list);
1353 auto cs = buf_cs.get_write_access(depends_list);
1354
1355 auto e = q.submit(depends_list, [&](sycl::handler &cgh) {
1356 cgh.parallel_for(sycl::range<1>{npart}, [=](sycl::item<1> item) {
1357 rho[item] = rho_in[item];
1358 P[item] = P_in[item];
1359 cs[item] = cs_in[item];
1360 });
1361 });
1362
1363 buf_rho_in.complete_event_state(e);
1364 buf_P_in.complete_event_state(e);
1365 buf_cs_in.complete_event_state(e);
1366 buf_rho.complete_event_state(e);
1367 buf_P.complete_event_state(e);
1368 buf_cs.complete_event_state(e);
1369 });
1370}
1371
1372template<class Tvec, template<class> class Kern>
1374 StackEntry stack_loc{};
1375
1376 // Only compute gradients for MUSCL reconstruction
1377 if (!solver_config.requires_gradients()) {
1378 return;
1379 }
1380
1381 using namespace shamrock;
1382 using namespace shamrock::patch;
1383
1384 const Tscal pmass = solver_config.gpart_mass;
1385 const Tscal gamma = solver_config.get_eos_gamma();
1386
1387 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
1388
1389 PatchDataLayerLayout &pdl = scheduler().pdl_old();
1390 const u32 ihpart = pdl.get_field_idx<Tscal>(gsph::names::common::hpart);
1391 const u32 ivxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::vxyz);
1392 const bool has_uint = solver_config.has_field_uint();
1393 const u32 iuint = has_uint ? pdl.get_field_idx<Tscal>(gsph::names::newtonian::uint) : 0;
1394
1395 // Get gradient fields from storage
1396 shamrock::solvergraph::Field<Tvec> &grad_density_field
1397 = shambase::get_check_ref(storage.grad_density);
1398 shamrock::solvergraph::Field<Tvec> &grad_pressure_field
1399 = shambase::get_check_ref(storage.grad_pressure);
1400 shamrock::solvergraph::Field<Tvec> &grad_vx_field = shambase::get_check_ref(storage.grad_vx);
1401 shamrock::solvergraph::Field<Tvec> &grad_vy_field = shambase::get_check_ref(storage.grad_vy);
1402 shamrock::solvergraph::Field<Tvec> &grad_vz_field = shambase::get_check_ref(storage.grad_vz);
1403
1404 // Get density field for SPH-summation density
1405 shamrock::solvergraph::Field<Tscal> &density_field = shambase::get_check_ref(storage.density);
1406
1407 // Ensure gradient fields have correct sizes
1408 shambase::DistributedData<u32> &counts = shambase::get_check_ref(storage.part_counts).indexes;
1409
1410 grad_density_field.ensure_sizes(counts);
1411 grad_pressure_field.ensure_sizes(counts);
1412 grad_vx_field.ensure_sizes(counts);
1413 grad_vy_field.ensure_sizes(counts);
1414 grad_vz_field.ensure_sizes(counts);
1415
1416 auto &merged_xyzh = storage.merged_xyzh.get();
1417 auto &neigh_cache = storage.neigh_cache->neigh_cache;
1418
1419 static constexpr Tscal Rkern = Kernel::Rkern;
1420
1421 // Compute gradients following reference implementation (g_pre_interaction.cpp)
1422 // grad_d = \sigma_j m_j \nabla W_ij
1423 // grad_p = (grad_d * u_i + du) * (\gamma - 1) where du = \sigma_j m_j (u_j - u_i) \nabla W_ij
1424 // grad_v = \sigma_j m_j (v_j - v_i) \nabla W_ij / \rho_i
1425 scheduler().for_each_patchdata_nonempty([&](const Patch p, PatchDataLayer &pdat) {
1426 u32 cnt = pdat.get_obj_cnt();
1427 if (cnt == 0)
1428 return;
1429
1430 auto &mfield = merged_xyzh.get(p.id_patch);
1431 auto &pcache = neigh_cache.get(p.id_patch);
1432
1433 // Get position, h, velocity from merged data
1434 auto &buf_xyz = mfield.template get_field_buf_ref<Tvec>(0);
1435 auto &buf_hpart = mfield.template get_field_buf_ref<Tscal>(1);
1436 auto &buf_vxyz = pdat.get_field_buf_ref<Tvec>(ivxyz);
1437
1438 // Get density (local particles only)
1439 auto &dens_field = density_field.get_field(p.id_patch);
1440
1441 // Get gradient output fields
1442 auto &grad_d_field = grad_density_field.get_field(p.id_patch);
1443 auto &grad_p_field = grad_pressure_field.get_field(p.id_patch);
1444 auto &grad_vx_buf = grad_vx_field.get_field(p.id_patch);
1445 auto &grad_vy_buf = grad_vy_field.get_field(p.id_patch);
1446 auto &grad_vz_buf = grad_vz_field.get_field(p.id_patch);
1447
1448 sham::DeviceQueue &q = dev_sched->get_queue();
1449 sham::EventList depends_list;
1450
1451 auto ploop_ptrs = pcache.get_read_access(depends_list);
1452 auto xyz_acc = buf_xyz.get_read_access(depends_list);
1453 auto h_acc = buf_hpart.get_read_access(depends_list);
1454 auto v_acc = buf_vxyz.get_read_access(depends_list);
1455 auto dens_acc = dens_field.get_buf().get_read_access(depends_list);
1456 auto grad_d_acc = grad_d_field.get_buf().get_write_access(depends_list);
1457 auto grad_p_acc = grad_p_field.get_buf().get_write_access(depends_list);
1458 auto grad_vx_acc = grad_vx_buf.get_buf().get_write_access(depends_list);
1459 auto grad_vy_acc = grad_vy_buf.get_buf().get_write_access(depends_list);
1460 auto grad_vz_acc = grad_vz_buf.get_buf().get_write_access(depends_list);
1461
1462 // Get internal energy if adiabatic
1463 const Tscal *uint_ptr = nullptr;
1464 if (has_uint) {
1465 uint_ptr = pdat.get_field_buf_ref<Tscal>(iuint).get_read_access(depends_list);
1466 }
1467
1468 auto e = q.submit(depends_list, [&](sycl::handler &cgh) {
1469 shamrock::tree::ObjectCacheIterator particle_looper(ploop_ptrs);
1470
1471 shambase::parallel_for(cgh, cnt, "gsph_compute_gradients", [=](u64 gid) {
1472 u32 id_a = (u32) gid;
1473
1474 Tvec xyz_a = xyz_acc[id_a];
1475 Tscal h_a = h_acc[id_a];
1476 Tvec v_a = v_acc[id_a];
1477 Tscal rho_a = sycl::max(dens_acc[id_a], Tscal(1e-30));
1478 Tscal dint = h_a * h_a * Rkern * Rkern;
1479
1480 // Get internal energy for particle a
1481 Tscal u_a = Tscal(0);
1482 if (uint_ptr != nullptr) {
1483 u_a = uint_ptr[id_a];
1484 }
1485
1486 // Initialize gradient accumulators
1487 Tvec grad_d = {0, 0, 0}; // Density gradient
1488 Tvec grad_u = {0, 0, 0}; // Internal energy difference gradient
1489 Tvec grad_vx = {0, 0, 0}; // Velocity component gradients
1490 Tvec grad_vy = {0, 0, 0};
1491 Tvec grad_vz = {0, 0, 0};
1492
1493 particle_looper.for_each_object(id_a, [&](u32 id_b) {
1494 Tvec dr = xyz_a - xyz_acc[id_b];
1495 Tscal rab2 = sycl::dot(dr, dr);
1496
1497 if (rab2 > dint || id_a == id_b) {
1498 return;
1499 }
1500
1501 Tscal rab = sycl::sqrt(rab2);
1502
1503 // Kernel gradient: \nabla W = (dW/dr) * (r/|r|)
1504 Tscal dWdr = Kernel::dW_3d(rab, h_a);
1505 Tvec gradW = dr * (dWdr * sham::inv_sat_positive(rab));
1506
1507 // Accumulate gradients (reference: g_pre_interaction.cpp lines 130-147)
1508 grad_d += gradW * pmass;
1509
1510 // Internal energy gradient for pressure
1511 Tscal u_b = (uint_ptr != nullptr) ? uint_ptr[id_b] : Tscal(0);
1512 grad_u += gradW * (pmass * (u_b - u_a));
1513
1514 // Velocity gradients
1515 Tvec v_b = v_acc[id_b];
1516 grad_vx += gradW * (pmass * (v_b[0] - v_a[0]));
1517 grad_vy += gradW * (pmass * (v_b[1] - v_a[1]));
1518 grad_vz += gradW * (pmass * (v_b[2] - v_a[2]));
1519 });
1520
1521 // Store density gradient
1522 grad_d_acc[id_a] = grad_d;
1523
1524 // Compute pressure gradient: \nabla P = (\nabla \rho * u + du) * (\gamma - 1)
1525 // (reference: g_pre_interaction.cpp line 143)
1526 Tvec grad_p = (grad_d * u_a + grad_u) * (gamma - Tscal(1));
1527 grad_p_acc[id_a] = grad_p;
1528
1529 // Normalize velocity gradients by density
1530 // (reference: g_pre_interaction.cpp lines 144-147)
1531 Tscal rho_inv = sham::inv_sat_positive(rho_a);
1532 grad_vx_acc[id_a] = grad_vx * rho_inv;
1533 grad_vy_acc[id_a] = grad_vy * rho_inv;
1534 grad_vz_acc[id_a] = grad_vz * rho_inv;
1535 });
1536 });
1537
1538 // Complete event states
1539 pcache.complete_event_state({e});
1540 buf_xyz.complete_event_state(e);
1541 buf_hpart.complete_event_state(e);
1542 buf_vxyz.complete_event_state(e);
1543 dens_field.get_buf().complete_event_state(e);
1544 grad_d_field.get_buf().complete_event_state(e);
1545 grad_p_field.get_buf().complete_event_state(e);
1546 grad_vx_buf.get_buf().complete_event_state(e);
1547 grad_vy_buf.get_buf().complete_event_state(e);
1548 grad_vz_buf.get_buf().complete_event_state(e);
1549 if (has_uint) {
1550 pdat.get_field_buf_ref<Tscal>(iuint).complete_event_state(e);
1551 }
1552 });
1553}
1554
1555template<class Tvec, template<class> class Kern>
1556void shammodels::gsph::Solver<Tvec, Kern>::prepare_corrector() {
1557 StackEntry stack_loc{};
1558
1559 shamrock::SchedulerUtility utility(scheduler());
1560 shamrock::patch::PatchDataLayerLayout &pdl = scheduler().pdl_old();
1561
1562 const u32 iaxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::axyz);
1563
1564 // Create compute field to store old acceleration
1565 auto old_axyz = utility.make_compute_field<Tvec>(gsph::names::internal::old_axyz, 1);
1566
1567 // Copy current acceleration to old_axyz
1568 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
1569
1570 scheduler().for_each_patchdata_nonempty(
1572 u32 cnt = pdat.get_obj_cnt();
1573 if (cnt == 0)
1574 return;
1575
1576 auto &axyz_field = pdat.get_field<Tvec>(iaxyz);
1577 auto &old_axyz_field = old_axyz.get_field(p.id_patch);
1578
1579 // Copy using kernel_call
1581 dev_sched->get_queue(),
1582 sham::MultiRef{axyz_field.get_buf()},
1583 sham::MultiRef{old_axyz_field.get_buf()},
1584 cnt,
1585 [](u32 i, const Tvec *src, Tvec *dst) {
1586 dst[i] = src[i];
1587 });
1588 });
1589
1590 storage.old_axyz.set(std::move(old_axyz));
1591
1592 if (solver_config.has_field_uint()) {
1593 const u32 iduint = pdl.get_field_idx<Tscal>(gsph::names::newtonian::duint);
1594 auto old_duint = utility.make_compute_field<Tscal>(gsph::names::internal::old_duint, 1);
1595
1596 scheduler().for_each_patchdata_nonempty(
1598 u32 cnt = pdat.get_obj_cnt();
1599 if (cnt == 0)
1600 return;
1601
1602 auto &duint_field = pdat.get_field<Tscal>(iduint);
1603 auto &old_duint_field = old_duint.get_field(p.id_patch);
1604
1605 // Copy using kernel_call
1607 dev_sched->get_queue(),
1608 sham::MultiRef{duint_field.get_buf()},
1609 sham::MultiRef{old_duint_field.get_buf()},
1610 cnt,
1611 [](u32 i, const Tscal *src, Tscal *dst) {
1612 dst[i] = src[i];
1613 });
1614 });
1615
1616 storage.old_duint.set(std::move(old_duint));
1617 }
1618}
1619
1620template<class Tvec, template<class> class Kern>
1622 StackEntry stack_loc{};
1623 // GSPH derivative update using Riemann solver
1624 gsph::modules::UpdateDerivs<Tvec, Kern>(context, solver_config, storage).update_derivs();
1625}
1626
1627template<class Tvec, template<class> class Kern>
1628typename shammodels::gsph::Solver<Tvec, Kern>::Tscal shammodels::gsph::Solver<Tvec, Kern>::
1630 StackEntry stack_loc{};
1631
1632 using namespace shamrock;
1633 using namespace shamrock::patch;
1634
1635 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
1636
1637 PatchDataLayerLayout &pdl = scheduler().pdl_old();
1638 const u32 ihpart = pdl.get_field_idx<Tscal>(gsph::names::common::hpart);
1639 const u32 iaxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::axyz);
1640
1641 shamrock::solvergraph::Field<Tscal> &soundspeed_field
1642 = shambase::get_check_ref(storage.soundspeed);
1643
1644 Tscal C_cour = solver_config.cfl_config.cfl_cour;
1645 Tscal C_force = solver_config.cfl_config.cfl_force;
1646
1647 // Use ComputeField for proper reduction support
1648 shamrock::SchedulerUtility utility(scheduler());
1649 ComputeField<Tscal> cfl_dt = utility.make_compute_field<Tscal>("cfl_dt", 1);
1650
1651 scheduler().for_each_patchdata_nonempty([&](Patch cur_p, PatchDataLayer &pdat) {
1652 u32 cnt = pdat.get_obj_cnt();
1653 if (cnt == 0)
1654 return;
1655
1656 auto &buf_hpart = pdat.get_field_buf_ref<Tscal>(ihpart);
1657 auto &buf_axyz = pdat.get_field_buf_ref<Tvec>(iaxyz);
1658 auto &buf_cs = soundspeed_field.get_field(cur_p.id_patch).get_buf();
1659 auto &cfl_dt_buf = cfl_dt.get_buf_check(cur_p.id_patch);
1660
1661 sham::DeviceQueue &q = dev_sched->get_queue();
1662 sham::EventList depends_list;
1663
1664 auto hpart = buf_hpart.get_read_access(depends_list);
1665 auto axyz = buf_axyz.get_read_access(depends_list);
1666 auto cs = buf_cs.get_read_access(depends_list);
1667 auto cfl_dt_acc = cfl_dt_buf.get_write_access(depends_list);
1668
1669 auto e = q.submit(depends_list, [&](sycl::handler &cgh) {
1670 shambase::parallel_for(cgh, cnt, "gsph_compute_cfl_dt", [=](u64 gid) {
1671 u32 i = (u32) gid;
1672
1673 Tscal h_i = hpart[i];
1674 Tscal cs_i = cs[i];
1675 Tscal abs_a = sycl::length(axyz[i]);
1676
1677 // Guard against invalid values (NaN/Inf)
1678 if (!sycl::isfinite(h_i) || h_i <= Tscal(0))
1679 h_i = Tscal(1e-10);
1680 if (!sycl::isfinite(cs_i) || cs_i <= Tscal(0))
1681 cs_i = Tscal(1e-10);
1682 if (!sycl::isfinite(abs_a))
1683 abs_a = Tscal(1e30);
1684
1685 // Sound CFL condition: dt = C_cour * h / c_s
1686 // Following Kitajima et al. (2025) simple form for GSPH
1687 Tscal dt_c = C_cour * h_i / cs_i;
1688
1689 // Force condition: dt = C_force * sqrt(h / |a|)
1690 Tscal dt_f = C_force * sycl::sqrt(h_i / (abs_a + Tscal(1e-30)));
1691
1692 Tscal dt_min = sycl::min(dt_c, dt_f);
1693
1694 // Ensure a valid finite timestep with minimum floor
1695 if (!sycl::isfinite(dt_min) || dt_min <= Tscal(0)) {
1696 dt_min = Tscal(1e-10); // Minimum timestep floor
1697 }
1698
1699 cfl_dt_acc[i] = dt_min;
1700 });
1701 });
1702
1703 buf_hpart.complete_event_state(e);
1704 buf_axyz.complete_event_state(e);
1705 buf_cs.complete_event_state(e);
1706 cfl_dt_buf.complete_event_state(e);
1707 });
1708
1709 // Compute minimum across all patches on this rank
1710 Tscal rank_dt = cfl_dt.compute_rank_min();
1711
1712 // Guard against invalid reduction result
1713 if (!std::isfinite(rank_dt) || rank_dt <= Tscal(0)) {
1714 rank_dt = Tscal(1e-6); // Reasonable floor for SPH simulations
1715 }
1716
1717 // Global reduction across MPI ranks
1718 Tscal global_min_dt = shamalgs::collective::allreduce_min(rank_dt);
1719
1720 // Final safety floor to prevent simulation stalling
1721 // For typical SPH simulations, timestep should be O(h/cs) ~ O(1e-4)
1722 // Use 1e-6 as minimum floor to prevent extreme stalling
1723 const Tscal dt_min_floor = Tscal(1e-6);
1724 if (!std::isfinite(global_min_dt) || global_min_dt < dt_min_floor) {
1725 global_min_dt = dt_min_floor;
1726 }
1727
1728 return global_min_dt;
1729}
1730
1731template<class Tvec, template<class> class Kern>
1732bool shammodels::gsph::Solver<Tvec, Kern>::apply_corrector(Tscal dt, u64 Npart_all) {
1733 StackEntry stack_loc{};
1734
1735 shamrock::patch::PatchDataLayerLayout &pdl = scheduler().pdl_old();
1736
1737 const u32 ivxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::vxyz);
1738 const u32 iaxyz = pdl.get_field_idx<Tvec>(gsph::names::newtonian::axyz);
1739
1740 Tscal half_dt = Tscal{0.5} * dt;
1741
1742 // Corrector: v = v + 0.5*a_new*dt (completing the leapfrog kick)
1743 // Predictor already added 0.5*a_old*dt, so total is 0.5*(a_old + a_new)*dt
1744 scheduler().for_each_patchdata_nonempty(
1746 u32 cnt = pdat.get_obj_cnt();
1747 if (cnt == 0)
1748 return;
1749
1750 auto &vxyz = pdat.get_field<Tvec>(ivxyz);
1751 auto &axyz = pdat.get_field<Tvec>(iaxyz);
1752
1753 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
1754
1756 dev_sched->get_queue(),
1757 sham::MultiRef{axyz.get_buf()},
1758 sham::MultiRef{vxyz.get_buf()},
1759 cnt,
1760 [half_dt](u32 i, const Tvec *axyz_new, Tvec *vxyz) {
1761 vxyz[i] += half_dt * axyz_new[i];
1762 });
1763 });
1764
1765 if (solver_config.has_field_uint()) {
1766 const u32 iuint = pdl.get_field_idx<Tscal>(gsph::names::newtonian::uint);
1767 const u32 iduint = pdl.get_field_idx<Tscal>(gsph::names::newtonian::duint);
1768
1769 scheduler().for_each_patchdata_nonempty(
1771 u32 cnt = pdat.get_obj_cnt();
1772 if (cnt == 0)
1773 return;
1774
1775 auto &uint_field = pdat.get_field<Tscal>(iuint);
1776 auto &duint = pdat.get_field<Tscal>(iduint);
1777
1778 auto dev_sched = shamsys::instance::get_compute_scheduler_ptr();
1779
1780 // Corrector: u = u + 0.5*du_new*dt (completing the leapfrog)
1782 dev_sched->get_queue(),
1783 sham::MultiRef{duint.get_buf()},
1784 sham::MultiRef{uint_field.get_buf()},
1785 cnt,
1786 [half_dt](u32 i, const Tscal *duint_new, Tscal *uint) {
1787 uint[i] += half_dt * duint_new[i];
1788 });
1789 });
1790
1791 storage.old_duint.reset();
1792 }
1793
1794 storage.old_axyz.reset();
1795
1796 return true;
1797}
1798
1799template<class Tvec, template<class> class Kern>
1800void shammodels::gsph::Solver<Tvec, Kern>::update_sync_load_values() {
1801 modules::ComputeLoadBalanceValue<Tvec, Kern>(context, solver_config, storage)
1802 .update_load_balancing();
1803 scheduler().scheduler_step(false, false);
1804}
1805
1806template<class Tvec, template<class> class Kern>
1808
1809 // Validate configuration before running
1810 solver_config.check_config_runtime();
1811
1812 Tscal t_current = get_time();
1813 Tscal dt = get_dt();
1814
1815 StackEntry stack_loc{};
1816
1817 if (shamcomm::world_rank() == 0) {
1819 sham::format("---------------- GSPH t = {}, dt = {} ----------------", t_current, dt));
1820 }
1821
1822 shambase::Timer tstep;
1823 tstep.start();
1824
1825 // Load balancing step
1826 gsph::modules::ComputeLoadBalanceValue<Tvec, Kern>(context, solver_config, storage)
1827 .update_load_balancing();
1828 scheduler().scheduler_step(true, true);
1829 gsph::modules::ComputeLoadBalanceValue<Tvec, Kern>(context, solver_config, storage)
1830 .update_load_balancing();
1831 scheduler().scheduler_step(false, false);
1832
1834
1835 using namespace shamrock;
1836 using namespace shamrock::patch;
1837
1838 u64 Npart_all = scheduler().get_total_obj_count();
1839
1840 // =========================================================================
1841 // CORRECTED SIMULATION LOOP ORDER (matching reference SPH code)
1842 // =========================================================================
1843 // The key insight from the reference code is that density/EOS must be
1844 // computed AFTER the predictor step, on the NEW positions. Otherwise,
1845 // the forces are computed using stale EOS values.
1846 //
1847 // Loop order:
1848 // 1. PREDICTOR: move particles using OLD accelerations
1849 // 2. BOUNDARY: apply periodic/free boundary conditions
1850 // 3. PRESTEP: converge smoothing length h, then compute omega and density
1851 // (density is now computed inside gsph_prestep(), on the same
1852 // converged h / merged positions / neighbor cache - see compute_density())
1853 // 4. DENSITY/EOS: compute density, pressure, soundspeed on NEW positions
1854 // 5. FORCES: compute accelerations using FRESH EOS
1855 // 6. CORRECTOR: refine velocities using average of old/new accelerations
1856 // 7. CFL: compute next timestep
1857 // =========================================================================
1858
1859 // STEP 1: PREDICTOR - move particles using OLD accelerations
1860 // (On first iteration, accelerations are zero, so this is just position drift)
1861 do_predictor_leapfrog(dt);
1862
1863 // STEP 2: BOUNDARY - apply boundary conditions to NEW positions
1864 // Build serial patch tree first (needed for boundary application)
1865 gen_serial_patch_tree();
1866 apply_position_boundary(t_current + dt);
1867
1868 // STEP 3: PRESTEP - converges h, builds ghost/tree/neighbor-cache state on
1869 // the new positions, and computes omega + density (see gsph_prestep() /
1870 // compute_density()). Everything it builds (ghost handler, ghost cache,
1871 // merged positions, merged trees, rint field, neighbor cache) stays valid
1872 // and is reused below - no need to rebuild it a second time here.
1873 gsph_prestep(t_current, dt);
1874
1875 // STEP 4a: GRADIENTS - compute for MUSCL reconstruction (if enabled)
1876 // Computed BEFORE ghost communication so gradients are included in ghost data
1877 // Gradients are computed on local particles using neighbor data
1879
1880 // Initialize ghost layout BEFORE communication
1881 // (includes gradients if MUSCL is enabled)
1882 init_ghost_layout();
1883
1884 // Communicate ghost fields (hpart, uint, vxyz, omega, and gradients if MUSCL)
1885 // This MUST happen BEFORE compute_eos_fields so EOS can be computed for ghosts
1886 communicate_merge_ghosts_fields();
1887
1888 // STEP 4b: EOS - compute AFTER ghost communication (CRITICAL!)
1889 // This ensures P and cs are computed for ALL particles (local + ghost)
1890 // Following SPH pattern: EOS is computed on merged_patchdata_ghost
1891 compute_eos_fields();
1892
1893 // STEP 5: FORCES - compute accelerations using FRESH EOS
1894 // Save old accelerations for corrector
1895 prepare_corrector();
1896
1897 // Update derivatives using GSPH Riemann solver
1898 update_derivs();
1899
1900 // STEP 6: CORRECTOR - refine velocities
1901 apply_corrector(dt, Npart_all);
1902
1903 // STEP 7: CFL - compute next timestep
1904 Tscal dt_next = compute_dt_cfl();
1905
1906 // Ensure dt doesn't grow too fast (max 2x per step), but allow any value if dt was 0
1907 if (dt > Tscal(0)) {
1908 dt_next = sham::min(dt_next, Tscal(2) * dt);
1909 }
1910
1911 // Copy EOS fields to patchdata for persistence and VTKDump access
1913
1914 // Cleanup for next iteration
1915 reset_neighbors_cache();
1916 reset_presteps_rint();
1917 clear_merged_pos_trees();
1918 reset_merge_ghosts_fields();
1919 storage.merged_xyzh.reset();
1920 clear_ghost_cache();
1921 reset_serial_patch_tree();
1922 reset_ghost_handler();
1923 storage.ghost_layout.reset();
1924
1925 // Update time
1926 set_time(t_current + dt);
1927 set_next_dt(dt_next);
1928
1929 solve_logs.step_count++;
1930
1931 tstep.stop();
1932
1933 // Prepare timing log
1934 TimestepLog log;
1935 log.rank = shamcomm::world_rank();
1936 log.rate = Tscal(Npart_all) / tstep.elapsed_sec();
1937 log.npart = Npart_all;
1938 log.tcompute = tstep.elapsed_sec();
1939
1940 return log;
1941}
1942
1943// Template instantiations
1944using namespace shammath;
1945
1946// M-spline kernels (Monaghan)
1950
1951// Wendland kernels (C2, C4, C6) - recommended for GSPH (Inutsuka 2002)
Constants for field names in GSPH solver, organized by physics mode.
constexpr const char * duint
Time derivative of internal energy du/dt.
constexpr const char * axyz
3-acceleration field
constexpr const char * uint
Specific internal energy u.
constexpr const char * vxyz
3-velocity field
constexpr const char * pos_merged
Position merged references (for h-iteration).
constexpr const char * old_axyz
Old acceleration (for corrector step).
constexpr const char * xyz
Position field (3D coordinates).
constexpr const char * density
Density \rho (derived from h).
constexpr const char * eps_h
Epsilon h references (for h-iteration convergence).
constexpr const char * soundspeed
Sound speed c_s (derived from EOS).
constexpr const char * pressure
Pressure P (derived from EOS).
constexpr const char * hpart
Smoothing length field.
constexpr const char * neigh_cache
Neighbor cache.
constexpr const char * omega
Grad-h correction factor \Omega.
GSPH-specific utilities for ghost handling.
shambase::DistributedData< PatchDataFieldRef< T > > DDPatchDataFieldRef
Alias for a DistributedData of PatchDataFieldRefs.
Declares the IterateSmoothingLengthDensityNeighLim module for iterating smoothing length based on the...
Declares the IterateSmoothingLengthDensity module for iterating smoothing length based on the SPH den...
Declares the LoopSmoothingLengthIter module for looping over the smoothing length iteration until con...
Header file describing a Node Instance.
sycl::queue & get_compute_queue(u32 id=0)
MPI scheduler.
Header file for the patch struct and related function.
std::uint32_t u32
32 bit unsigned integer
std::uint64_t u64
64 bit unsigned integer
The MPI scheduler.
A buffer allocated in USM (Unified Shared Memory).
void complete_event_state(sycl::event e) const
Complete the event state of the buffer.
T * get_write_access(sham::EventList &depends_list, SourceLocation src_loc=SourceLocation{})
Get a read-write pointer to the buffer's data.
const T * get_read_access(sham::EventList &depends_list, SourceLocation src_loc=SourceLocation{}) const
Get a read-only pointer to the buffer's data.
A SYCL queue associated with a device and a context.
sycl::event submit(Fct &&fct)
Submits a kernel to the SYCL queue.
Class to manage a list of SYCL events.
Definition EventList.hpp:32
Container for objects shared between two distributed data elements.
void for_each(std::function< void(u64, u64, T &)> &&f)
Apply a function to all stored objects.
Represents a collection of objects distributed across patches identified by a u64 id.
Class Timer measures the time elapsed since the timer was started.
Definition Timer.hpp:36
f64 elapsed_sec() const
Converts the stored nanosecond time to a floating point representation in seconds.
Definition Timer.hpp:88
void start()
Starts the timer.
Definition Timer.hpp:51
void stop()
Stops the timer and stores the elapsed time in nanoseconds.
Definition Timer.hpp:65
GSPH-specific utilities for ghost particle handling.
The GSPH Solver class.
Definition Solver.hpp:71
void compute_density()
Compute SPH-summation density for GSPH.
Definition Solver.cpp:543
void copy_eos_to_patchdata()
Copy EOS fields from solvergraph to patchdata for persistence.
Definition Solver.cpp:1311
void compute_gradients()
Compute gradients for MUSCL reconstruction.
Definition Solver.cpp:1373
TimestepLog evolve_once()
Definition Solver.cpp:1807
void update_derivs()
Update derivatives using GSPH Riemann solver.
Definition Solver.cpp:1621
Tscal compute_dt_cfl()
Compute CFL timestep constraint.
Definition Solver.cpp:1629
GSPH derivative update module.
void update_derivs()
Update all derivatives using GSPH Riemann solver approach.
Utility class used to move the objects between patches.
ComputeField< T > make_compute_field(std::string new_name, u32 nvar)
create a compute field and init it to zeros
u32 get_field_idx(const std::string &field_name) const
Get the field id if matching name & type.
PatchDataLayer container class, the layout is described in patchdata_layout.
u32 get_obj_cnt() const
get the number of objects (particles) stored in this layer
virtual void ensure_sizes(const shambase::DistributedData< u32 > &sizes)
Ensure that the sizes of the patches in the field match the given sizes (Can resize the underlying fi...
Definition Field.hpp:92
PatchDataField< T > & get_field(u64 id) const
Get the underlying PatchDataField at the given id.
A Compressed Leaf Bounding Volume Hierarchy (CLBVH) for neighborhood queries.
A data structure representing a Karras Radix Tree Field.
Class holding the value of numerous constants generated from the following source.
This header file contains utility functions related to exception handling in the code.
MPI string gather / allgather helpers (declarations; implementations in shamalgs/src/collective/gathe...
Configuration for the Godunov SPH (GSPH) solver.
GSPH Solver class.
GSPH derivative update module.
VTK dump module for GSPH solver.
T inv_sat_positive(T v, T minvsat=T{1e-9}, T satval=T{0.}) noexcept
inverse saturated (positive numbers only)
Definition math.hpp:841
void kernel_call(sham::DeviceQueue &q, RefIn in, RefOut in_out, u32 n, Functor &&func, SourceLocation &&callsite=SourceLocation{})
Submit a kernel to a SYCL queue.
void throw_with_loc(std::string message, SourceLocation loc=SourceLocation{})
Throw an exception and append the source location to it.
T & get_check_ref(const std::unique_ptr< T > &ptr, SourceLocation loc=SourceLocation())
Takes a std::unique_ptr and returns a reference to the object it holds. It throws a std::runtime_erro...
Definition memory.hpp:112
i32 world_rank()
Gives the rank of the current process in the MPI communicator.
Definition worldInfo.cpp:41
namespace for math utility
Definition AABB.hpp:26
namespace for the main framework
Definition __init__.py:1
void raw_ln(Types... var2)
Prints a log message with multiple arguments followed by a newline.
Definition logs.hpp:89
void info_ln(std::string module_name, Types... var2)
Prints a log message with multiple arguments followed by a newline.
Definition logs.hpp:132
void warn_ln(std::string module_name, Types... var2)
Prints a log message with multiple arguments followed by a newline.
Definition logs.hpp:132
void err_ln(std::string module_name, Types... var2)
Prints a log message with multiple arguments followed by a newline.
Definition logs.hpp:132
sph kernels
shambase::details::BasicStackEntry StackEntry
Alias for shambase::details::BasicStackEntry.
A class that references multiple buffers or similar objects.
Definition MultiRef.hpp:33
Axis-Aligned bounding box.
Definition AABB.hpp:99
T lower
Lower bound of the AABB.
Definition AABB.hpp:104
T upper
Upper bound of the AABB.
Definition AABB.hpp:105
Patch object that contain generic patch information.
Definition Patch.hpp:33
u64 id_patch
unique key that identify the patch
Definition Patch.hpp:86
Functions related to the MPI communicator.