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