Shamrock 2025.10.0
Astrophysical Code
Loading...
Searching...
No Matches
pyGSPHModel.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
27#include "shambase/memory.hpp"
35#include <pybind11/cast.h>
36#include <pybind11/numpy.h>
37#include <memory>
38
39template<class Tvec, template<class> class SPHKernel>
40void add_gsph_instance(py::module &m, std::string name_config, std::string name_model) {
41 using namespace shammodels::gsph;
42
43 using Tscal = shambase::VecComponent<Tvec>;
44
45 using T = Model<Tvec, SPHKernel>;
46 using TConfig = typename T::SolverConfig;
47
48 shamlog_debug_ln("[Py]", "registering class :", name_config, typeid(T).name());
49 shamlog_debug_ln("[Py]", "registering class :", name_model, typeid(T).name());
50
51 py::class_<TConfig> config_cls(m, name_config.c_str());
52
53 shammodels::common::add_json_defs<TConfig>(config_cls);
54
55 config_cls.def("print_status", &TConfig::print_status)
56 .def("set_tree_reduction_level", &TConfig::set_tree_reduction_level)
57 .def(
58 "set_neigh_cache_strategy",
59 &TConfig::set_neigh_cache_strategy,
60 R"==(
61 Set the strategy used to build the neighbours cache.
62
63 Parameters
64 ----------
65 strategy : NeighCacheStrategy
66 Either ``NeighCacheStrategy.SingleStage`` or ``NeighCacheStrategy.TwoStage``
67 (the default), as obtained from ``from shamrock import NeighCacheStrategy``.
68)==")
69 .def(
70 "set_two_stage_search",
71 &TConfig::set_two_stage_search,
72 R"==(
73 Set the neighbours cache strategy from a boolean.
74
75 .. deprecated::
76 Use :py:meth:`set_neigh_cache_strategy` instead.
77)==")
78 // Riemann solver config
79 .def(
80 "set_riemann_iterative",
81 [](TConfig &self, Tscal tol, u32 max_iter) {
82 self.set_riemann_iterative(tol, max_iter);
83 },
84 py::kw_only(),
85 py::arg("tolerance") = Tscal{1e-6},
86 py::arg("max_iter") = 20,
87 R"==(
88 Set iterative Riemann solver (van Leer 1997).
89
90 This is the most accurate but slower Riemann solver.
91 Uses Newton-Raphson iteration to find the pressure in the star region.
92
93 Parameters
94 ----------
95 tolerance : float
96 Convergence tolerance for Newton-Raphson iteration (default: 1e-6)
97 max_iter : int
98 Maximum number of iterations (default: 20)
99)==")
100 .def(
101 "set_riemann_hllc",
102 [](TConfig &self) {
103 self.set_riemann_hllc();
104 },
105 R"==(
106 Set HLLC approximate Riemann solver.
107
108 Fast approximate Riemann solver that captures contact discontinuities.
109 Recommended for general use - good balance of accuracy and speed.
110)==")
111 .def(
112 "set_riemann_exact",
113 [](TConfig &self, Tscal tol, u32 max_iter) {
114 self.set_riemann_exact(tol, max_iter);
115 },
116 py::kw_only(),
117 py::arg("tolerance") = Tscal{1e-8},
118 py::arg("max_iter") = 100,
119 R"==(
120 Set exact Riemann solver (Toro 2009).
121
122 Classifies the wave pattern (shock/rarefaction on each side) from the
123 initial states, then solves the matching closed-form relation via
124 bisection. Most accurate but computationally expensive; unlike the
125 iterative (van Leer) solver, it also remains accurate for strong
126 rarefactions / near-vacuum conditions.
127
128 Parameters
129 ----------
130 tolerance : float
131 Bisection convergence tolerance (default: 1e-8)
132 max_iter : int
133 Maximum number of bisection iterations (default: 100)
134)==")
135 // Reconstruction config
136 .def(
137 "set_reconstruct_piecewise_constant",
138 [](TConfig &self) {
139 self.set_reconstruct_piecewise_constant();
140 },
141 R"==(
142 Set first-order piecewise constant reconstruction.
143
144 Sets all gradients to zero. Most diffusive but most stable.
145 Good for very strong shocks or initial testing.
146)==")
147 // Force formulation config
148 .def(
149 "set_force_cha_whitworth",
150 [](TConfig &self) {
151 self.set_force_cha_whitworth();
152 },
153 R"==(
154 Set the Cha & Whitworth (2003) symmetric SPH force formulation (default).
155
156 Uses the standard SPH momentum equation (nabla_W/rho^2/Omega) with the
157 Riemann-solved interface pressure p* substituted for pressure.
158)==")
159 .def(
160 "set_force_inutsuka_v2",
161 [](TConfig &self) {
162 self.set_force_inutsuka_v2();
163 },
164 R"==(
165 Set the Inutsuka (2002) effective volume/face force formulation.
166
167 Uses linear (1st order) interpolation of the volume element between each
168 particle pair to build an effective face (V2_ij, s*), following the
169 original GSPH momentum equation: acc -= m * p* * V2_ij * grad_W_ij.
170)==")
171 // EOS config
172 .def(
173 "set_eos_adiabatic",
174 [](TConfig &self, Tscal gamma) {
175 self.set_eos_adiabatic(gamma);
176 },
177 py::arg("gamma"),
178 R"==(
179 Set adiabatic equation of state: P = (\gamma-1) \rho u
180
181 Parameters
182 ----------
183 gamma : float
184 Adiabatic index (e.g., 5/3 for monatomic gas, 7/5 for diatomic)
185)==")
186 .def(
187 "set_eos_isothermal",
188 [](TConfig &self, Tscal cs) {
189 self.set_eos_isothermal(cs);
190 },
191 py::arg("cs"),
192 R"==(
193 Set isothermal equation of state: P = cs^2 \rho
194
195 Parameters
196 ----------
197 cs : float
198 Sound speed
199)==")
200 // Boundary config
201 .def("set_boundary_free", &TConfig::set_boundary_free)
202 .def("set_boundary_periodic", &TConfig::set_boundary_periodic)
203 // External forces
204 .def(
205 "add_ext_force_point_mass",
206 [](TConfig &self, Tscal central_mass, Tscal Racc, Tvec central_pos) {
207 self.add_ext_force_point_mass(central_mass, Racc, central_pos);
208 },
209 py::arg("central_mass"),
210 py::arg("Racc"),
211 py::kw_only(),
212 py::arg("central_pos") = Tvec{0, 0, 0})
213 // Units
214 .def("set_units", &TConfig::set_units)
215 // CFL
216 .def(
217 "set_cfl_cour",
218 [](TConfig &self, Tscal cfl_cour) {
219 self.cfl_config.cfl_cour = cfl_cour;
220 })
221 .def(
222 "set_cfl_force",
223 [](TConfig &self, Tscal cfl_force) {
224 self.cfl_config.cfl_force = cfl_force;
225 })
226 .def(
227 "set_particle_mass",
228 [](TConfig &self, Tscal gpart_mass) {
229 self.gpart_mass = gpart_mass;
230 })
231 .def(
232 "set_scheduler_config",
233 [](TConfig &self, u64 split_crit, u64 merge_crit) {
234 self.scheduler_conf.split_load_value = split_crit;
235 self.scheduler_conf.merge_load_value = merge_crit;
236 },
237 py::kw_only(),
238 py::arg("split_load_value"),
239 py::arg("merge_load_value"));
240
241 py::class_<T>(m, name_model.c_str())
242 .def(py::init([](ShamrockCtx &ctx) {
243 return std::make_unique<T>(ctx);
244 }))
245 .def("init", &T::init)
246 .def("init_scheduler", &T::init_scheduler)
247 .def("evolve_once", &T::evolve_once)
248 .def(
249 "evolve_until",
250 [](T &self, f64 target_time, i32 niter_max) {
251 return self.evolve_until(target_time, niter_max);
252 },
253 py::arg("target_time"),
254 py::kw_only(),
255 py::arg("niter_max") = -1)
256 .def("timestep", &T::timestep)
257 .def("set_cfl_cour", &T::set_cfl_cour, py::arg("cfl_cour"))
258 .def("set_cfl_force", &T::set_cfl_force, py::arg("cfl_force"))
259 .def("set_particle_mass", &T::set_particle_mass, py::arg("gpart_mass"))
260 .def("get_particle_mass", &T::get_particle_mass)
261 .def("rho_h", &T::rho_h)
262 .def("get_hfact", &T::get_hfact)
263 .def(
264 "get_box_dim_fcc_3d",
265 [](T &self, f64 dr, u32 xcnt, u32 ycnt, u32 zcnt) {
266 return self.get_box_dim_fcc_3d(dr, xcnt, ycnt, zcnt);
267 })
268 .def(
269 "get_ideal_fcc_box",
270 [](T &self, f64 dr, f64_3 box_min, f64_3 box_max) {
271 return self.get_ideal_fcc_box(dr, {box_min, box_max});
272 })
273 .def(
274 "get_ideal_hcp_box",
275 [](T &self, f64 dr, f64_3 box_min, f64_3 box_max) {
276 return self.get_ideal_hcp_box(dr, {box_min, box_max});
277 })
278 .def(
279 "resize_simulation_box",
280 [](T &self, f64_3 box_min, f64_3 box_max) {
281 return self.resize_simulation_box({box_min, box_max});
282 })
283 .def(
284 "add_cube_fcc_3d",
285 [](T &self, f64 dr, f64_3 box_min, f64_3 box_max) {
286 return self.add_cube_fcc_3d(dr, {box_min, box_max});
287 })
288 .def(
289 "add_cube_hcp_3d",
290 [](T &self, f64 dr, f64_3 box_min, f64_3 box_max) {
291 return self.add_cube_hcp_3d(dr, {box_min, box_max});
292 })
293 .def("get_total_part_count", &T::get_total_part_count)
294 .def("total_mass_to_part_mass", &T::total_mass_to_part_mass)
295 .def(
296 "set_field_in_box",
297 [](T &self,
298 std::string field_name,
299 std::string field_type,
300 pybind11::object value,
301 f64_3 box_min,
302 f64_3 box_max,
303 u32 ivar) {
304 if (field_type == "f64") {
305 f64 val = value.cast<f64>();
306 self.set_field_in_box(field_name, val, {box_min, box_max}, ivar);
307 } else if (field_type == "f64_3") {
308 f64_3 val = value.cast<f64_3>();
309 self.set_field_in_box(field_name, val, {box_min, box_max}, ivar);
310 } else if (field_type == "u32") {
311 u32 val = value.cast<u32>();
312 self.set_field_in_box(field_name, val, {box_min, box_max}, ivar);
313 } else {
315 "unknown field type: " + field_type + ". Valid types: f64, f64_3, u32");
316 }
317 },
318 py::arg("field_name"),
319 py::arg("field_type"),
320 py::arg("value"),
321 py::arg("box_min"),
322 py::arg("box_max"),
323 py::kw_only(),
324 py::arg("ivar") = 0,
325 R"==(
326 Set field value for particles within a box region.
327
328 Useful for setting up discontinuous initial conditions like Sod shock tube.
329
330 Parameters
331 ----------
332 field_name : str
333 Name of the field to set (e.g., "vxyz", "uint", "hpart")
334 field_type : str
335 Type of the field: "f64", "f64_3", or "u32"
336 value : float, tuple, or int
337 Value to set (type must match field_type)
338 box_min : tuple
339 Minimum corner of the box (x, y, z)
340 box_max : tuple
341 Maximum corner of the box (x, y, z)
342 ivar : int
343 Variable index for multi-component fields (default: 0)
344
345 Examples
346 --------
347 >>> # Sod shock tube: set left state internal energy
348 >>> model.set_field_in_box("uint", "f64", u_left, (-1,-1,-1), (0,1,1))
349 >>> # Set right state
350 >>> model.set_field_in_box("uint", "f64", u_right, (0,-1,-1), (1,1,1))
351)==")
352 .def(
353 "set_field_in_sphere",
354 [](T &self,
355 std::string field_name,
356 std::string field_type,
357 pybind11::object value,
358 f64_3 center,
359 f64 radius) {
360 if (field_type == "f64") {
361 f64 val = value.cast<f64>();
362 self.set_field_in_sphere(field_name, val, center, radius);
363 } else if (field_type == "f64_3") {
364 f64_3 val = value.cast<f64_3>();
365 self.set_field_in_sphere(field_name, val, center, radius);
366 } else {
368 "unknown field type");
369 }
370 },
371 py::arg("field_name"),
372 py::arg("field_type"),
373 py::arg("value"),
374 py::arg("center"),
375 py::arg("radius"),
376 R"==(
377 Set field value for particles within a spherical region.
378
379 Useful for setting up point-source initial conditions like Sedov blast.
380
381 Parameters
382 ----------
383 field_name : str
384 Name of the field to set (e.g., "uint")
385 field_type : str
386 Type of the field: "f64" or "f64_3"
387 value : float or tuple
388 Value to set (type must match field_type)
389 center : tuple
390 Center of the sphere (x, y, z)
391 radius : float
392 Radius of the sphere
393
394 Examples
395 --------
396 >>> # Sedov blast: inject energy in central sphere
397 >>> model.set_field_in_sphere("uint", "f64", u_blast, (0,0,0), r_blast)
398)==")
399 .def("apply_field_from_position_f64_3", &T::template apply_field_from_position<f64_3>)
400 .def("apply_field_from_position_f64", &T::template apply_field_from_position<f64>)
401 .def(
402 "get_sum",
403 [](T &self, std::string field_name, std::string field_type) {
404 if (field_type == "f64") {
405 return py::cast(self.template get_sum<f64>(field_name));
406 } else if (field_type == "f64_3") {
407 return py::cast(self.template get_sum<f64_3>(field_name));
408 } else {
410 "unknown field type");
411 }
412 })
413 .def(
414 "gen_default_config",
415 [](T &self) {
416 return self.gen_default_config();
417 })
418 .def(
419 "get_current_config",
420 [](T &self) {
421 return self.solver.solver_config;
422 })
423 .def("set_solver_config", &T::set_solver_config)
424 .def("do_vtk_dump", &T::do_vtk_dump)
425 .def("solver_logs_last_rate", &T::solver_logs_last_rate)
426 .def("solver_logs_last_obj_count", &T::solver_logs_last_obj_count)
427 .def(
428 "get_time",
429 [](T &self) {
430 return self.solver.get_time();
431 })
432 .def(
433 "get_dt",
434 [](T &self) {
435 return self.solver.get_dt();
436 })
437 .def(
438 "set_time",
439 [](T &self, Tscal t) {
440 return self.solver.set_time(t);
441 })
442 .def(
443 "set_next_dt",
444 [](T &self, Tscal dt) {
445 return self.solver.set_next_dt(dt);
446 })
447 .def(
448 "load_from_dump",
449 &T::load_from_dump,
450 py::arg("filename"),
451 R"==(
452 Load simulation state from a Shamrock dump file.
453
454 Uses the shared ShamrockDump mechanism (same as SPH).
455
456 Parameters
457 ----------
458 filename : str
459 Path to the dump file
460
461 Example
462 -------
463 >>> model.load_from_dump("checkpoint.shamrock")
464)==")
465 .def(
466 "dump",
467 &T::dump,
468 py::arg("filename"),
469 R"==(
470 Write simulation state to a Shamrock dump file.
471
472 Uses the shared ShamrockDump mechanism (same as SPH).
473
474 Parameters
475 ----------
476 filename : str
477 Path to the dump file
478
479 Example
480 -------
481 >>> model.dump("checkpoint.shamrock")
482)==");
483}
484
485using namespace shammodels::gsph;
486
488 auto &m = root_module;
489
490 py::module mgsph = m.def_submodule("model_gsph", "Shamrock GSPH (Godunov SPH) solver");
491
492 using namespace shammodels::gsph;
493
494 // Register GSPH models for different kernels
495 add_gsph_instance<f64_3, shammath::M4>(
496 mgsph, "GSPHModel_f64_3_M4_SolverConfig", "GSPHModel_f64_3_M4");
497 add_gsph_instance<f64_3, shammath::M6>(
498 mgsph, "GSPHModel_f64_3_M6_SolverConfig", "GSPHModel_f64_3_M6");
499 add_gsph_instance<f64_3, shammath::M8>(
500 mgsph, "GSPHModel_f64_3_M8_SolverConfig", "GSPHModel_f64_3_M8");
501
502 add_gsph_instance<f64_3, shammath::C2>(
503 mgsph, "GSPHModel_f64_3_C2_SolverConfig", "GSPHModel_f64_3_C2");
504 add_gsph_instance<f64_3, shammath::C4>(
505 mgsph, "GSPHModel_f64_3_C4_SolverConfig", "GSPHModel_f64_3_C4");
506 add_gsph_instance<f64_3, shammath::C6>(
507 mgsph, "GSPHModel_f64_3_C6_SolverConfig", "GSPHModel_f64_3_C6");
508
509 using VariantGSPHModelBind = std::variant<
510 std::unique_ptr<Model<f64_3, shammath::M4>>,
511 std::unique_ptr<Model<f64_3, shammath::M6>>,
512 std::unique_ptr<Model<f64_3, shammath::M8>>,
513 std::unique_ptr<Model<f64_3, shammath::C2>>,
514 std::unique_ptr<Model<f64_3, shammath::C4>>,
515 std::unique_ptr<Model<f64_3, shammath::C6>>>;
516
517 m.def(
518 "get_Model_GSPH",
519 [](ShamrockCtx &ctx, std::string vector_type, std::string kernel) -> VariantGSPHModelBind {
520 VariantGSPHModelBind ret;
521
522 if (vector_type == "f64_3" && kernel == "M4") {
523 ret = std::make_unique<Model<f64_3, shammath::M4>>(ctx);
524 } else if (vector_type == "f64_3" && kernel == "M6") {
525 ret = std::make_unique<Model<f64_3, shammath::M6>>(ctx);
526 } else if (vector_type == "f64_3" && kernel == "M8") {
527 ret = std::make_unique<Model<f64_3, shammath::M8>>(ctx);
528 } else if (vector_type == "f64_3" && kernel == "C2") {
529 ret = std::make_unique<Model<f64_3, shammath::C2>>(ctx);
530 } else if (vector_type == "f64_3" && kernel == "C4") {
531 ret = std::make_unique<Model<f64_3, shammath::C4>>(ctx);
532 } else if (vector_type == "f64_3" && kernel == "C6") {
533 ret = std::make_unique<Model<f64_3, shammath::C6>>(ctx);
534 } else {
536 "unknown combination of representation and kernel");
537 }
538
539 return ret;
540 },
541 py::kw_only(),
542 py::arg("context"),
543 py::arg("vector_type") = "f64_3",
544 py::arg("sph_kernel") = "M4",
545 R"==(
546 Create a GSPH (Godunov SPH) model.
547
548 GSPH uses Riemann solvers at particle interfaces instead of artificial viscosity,
549 giving sharper shock resolution.
550
551 Parameters
552 ----------
553 context : ShamrockCtx
554 Shamrock context
555 vector_type : str
556 Vector type, e.g., "f64_3" for 3D double precision (default: "f64_3")
557 sph_kernel : str
558 SPH kernel type: "M4" (cubic spline, default), "M6", "M8" (quintic spline),
559 "C2", "C4", "C6" (Wendland kernels)
560
561 Returns
562 -------
563 GSPHModel
564 A GSPH model instance
565
566 Examples
567 --------
568 >>> ctx = shamrock.ShamrockCtx()
569 >>> model = shamrock.get_Model_GSPH(context=ctx) # Uses M4 kernel by default
570 >>> config = model.gen_default_config()
571 >>> config.set_riemann_hllc()
572 >>> config.set_eos_adiabatic(1.4)
573 >>> model.set_solver_config(config)
574)==");
575}
MPI scheduler.
double f64
Alias for double.
std::uint32_t u32
32 bit unsigned integer
std::uint64_t u64
64 bit unsigned integer
std::int32_t i32
32 bit integer
The GSPH Model class.
Definition Model.hpp:63
This header file contains utility functions related to exception handling in the code.
GSPH Model class - high-level interface for GSPH simulations.
ExcptTypes make_except_with_loc(std::string message, SourceLocation loc=SourceLocation{})
Create an exception with a message and a location.
Pybind11 include and definitions.
#define ON_PYTHON_INIT
Register a Python module init function using static initialization.
Utilities to convert JSON objects to Python objects and vice versa. TODO: try to convert directly wit...
sph kernels
Functions related to the MPI communicator.