7This is a particle tracing simulation for volcanic plume rise using the Euler equations.
9In this one-way coupling setup, the fluid field (volcanic plume) affects the particles' motion,
10but the particles do not influence the fluid dynamics. The simulation models the rise of hot,
11buoyant volcanic material through cooler ambient air.
16 file_path: str =
"InitTracers.dat",
23 round_decimals: int = 6,
26 Generates a .dat file with particles on a uniform grid inside a rectangular plume.
29 file_path (str): Path to save the .dat file.
30 num_x (int): Number of points along x (>=1).
31 num_y (int): Number of points along y (>=1).
32 x_min (float): Minimum x of the plume.
33 x_max (float): Maximum x of the plume.
34 y_min (float): Minimum y of the plume.
35 y_max (float): Maximum y of the plume.
36 round_decimals (int): Decimal places for rounding/printing.
38 if num_x < 1
or num_y < 1:
39 raise ValueError(
"num_x and num_y must be >= 1")
41 raise ValueError(
"x_min must be <= x_max")
43 raise ValueError(
"y_min must be <= y_max")
46 x_values = [round((x_min + x_max) / 2.0, round_decimals)]
48 x_step = (x_max - x_min) / (num_x - 1)
49 x_values = [round(x_min + i * x_step, round_decimals)
for i
in range(num_x)]
52 y_values = [round((y_min + y_max) / 2.0, round_decimals)]
54 y_step = (y_max - y_min) / (num_y - 1)
55 y_values = [round(y_min + j * y_step, round_decimals)
for j
in range(num_y)]
57 with open(file_path,
"w")
as file:
60 file.write(f
"{x:.{round_decimals}f} {y:.{round_decimals}f}\n")
63 print(f
"Generated {total} particles inside plume region and saved to {file_path}")
66parser = exahype2.ArgumentParser(
67 "ExaHyPE 2 - Finite Volumes Particle Tracing Testing Script"
70interpolation_methods = {
79 help=
"|".join(interpolation_methods),
82integration_schemes = {
92 "-integ",
"--integration", default=
"RK4", help=
"|".join(integration_schemes)
97 degrees_of_freedom=64,
101args = parser.parse_args()
105max_h = 1.1 * min(size) / (3.0**args.min_depth)
106min_h = max_h * 3.0 ** (-args.amr_levels)
108project = exahype2.Project(
109 namespace=[
"tests",
"exahype2",
"fv"],
112 executable=
"ExaHyPE",
115if args.number_of_snapshots <= 0:
116 time_in_between_plots = 0.0
118 time_in_between_plots = args.end_time / args.number_of_snapshots
119 project.set_output_path(args.output)
121project.set_global_simulation_parameters(
125 min_end_time=args.end_time,
126 max_end_time=args.end_time,
127 first_plot_time_stamp=0.0,
128 time_in_between_plots=time_in_between_plots,
130 args.periodic_boundary_conditions_x,
131 args.periodic_boundary_conditions_y,
135initial_conditions =
"""
136 // Parameters for volcanic plume simulation using defined constants
137 Q[4] = 0.0; // Initialise plume indicator
139 const double plume_center_x = DomainSize[0] * 0.5;
140 const double plume_width = DomainSize[0] * 0.1;
141 const double plume_base_y = DomainSize[1] * 0.1;
142 const double plume_top_y = DomainSize[1] * 0.3;
144 // Identify if x is in the plume region
145 bool in_plume_column =
146 (x(0) >= plume_center_x - plume_width / 2.0) &&
147 (x(0) <= plume_center_x + plume_width / 2.0);
149 bool in_plume = in_plume_column &&
150 (x(1) >= plume_base_y) &&
151 (x(1) <= plume_top_y);
153 // Compute piecewise hydrostatic pressure
154 double p_local = 0.0;
156 if (in_plume_column) {
157 if (x(1) >= plume_top_y) {
158 // Above plume -> full cold column
159 p_local = P0 + GRAVITY * (DomainSize[1] - x(1)) * RHO_AMBIENT;
160 } else if (x(1) < plume_base_y) {
161 // Below plume -> lighter column above
163 GRAVITY * (DomainSize[1] - plume_top_y) * RHO_AMBIENT + // Cold part above plume
164 GRAVITY * (plume_top_y - plume_base_y) * RHO_HOT + // Plume region
165 GRAVITY * (plume_base_y - x(1)) * RHO_AMBIENT; // Local segment below plume
167 // Inside plume -> partial light column
169 GRAVITY * (DomainSize[1] - plume_top_y) * RHO_AMBIENT + // Cold part above plume
170 GRAVITY * (plume_top_y - x(1)) * RHO_HOT; // Current plume slice
174 // Outside plume column -> pure cold fluid
175 p_local = P0 + GRAVITY * (DomainSize[1] - x(1)) * RHO_AMBIENT;
178 // Energy from pressure
179 double e_local = p_local / (GAMMA - 1.0);
181 Q[0] = in_plume ? RHO_HOT : RHO_AMBIENT; // density
184 Q[3] = e_local; // total energy
187boundary_conditions =
"""
188 if (x(1) >= DomainSize[1] - 0.001) {
189 // Top boundary (outflow - zero gradient)
190 Qoutside[0] = RHO_AMBIENT;
193 Qoutside[3] = P0 / (GAMMA - 1.0);
194 } else if (x(1) <= DomainOffset[1] + 0.001) {
195 // Bottom boundary (reflective)
196 Qoutside[0] = Qinside[0];
197 Qoutside[1] = Qinside[1];
198 Qoutside[2] = -Qinside[2];
199 Qoutside[3] = Qinside[3];
200 } else if (x(0) <= DomainOffset[0] + 0.001 || x(0) >= DomainSize[0] - 0.001) {
201 // Left and right boundaries (outflow - zero gradient)
202 Qoutside[0] = Qinside[0];
203 Qoutside[1] = Qinside[1];
204 Qoutside[2] = Qinside[2];
205 Qoutside[3] = Qinside[3];
209compute_primitive_variables =
r"""
210 const auto irho = 1.0 / Q[Shortcuts::rho];
211 const auto u0 = Q[Shortcuts::rhoU] * irho;
212 const auto u1 = Q[Shortcuts::rhoV] * irho;
214 const auto uSq = u0 * u0 + u1 * u1;
215 const auto u_n = (normal == 0) ? u0 : u1;
217 const auto internalE = Q[Shortcuts::rhoE] - 0.5 * Q[Shortcuts::rho] * uSq;
218 const auto p = (GAMMA - 1.0) * internalE;
222 {compute_primitive_variables}
223 const auto speedOfSound = std::sqrt(GAMMA * p * irho);
224 auto result = std::fmax(0.0, std::fabs(u_n - speedOfSound));
225 result = std::fmax(result, std::fabs(u_n + speedOfSound));
228 compute_primitive_variables=compute_primitive_variables
232 {compute_primitive_variables}
234 F[Shortcuts::rho] = Q[Shortcuts::rhoU + normal];
236 F[Shortcuts::rhoU] = Q[Shortcuts::rhoU] * u_n;
237 F[Shortcuts::rhoV] = Q[Shortcuts::rhoV] * u_n;
239 F[Shortcuts::rhoU + normal] += p;
241 F[Shortcuts::rhoE] = (Q[Shortcuts::rhoE] + p) * u_n;
243 compute_primitive_variables=compute_primitive_variables
249 S[2] = -GRAVITY * Q[0];
250 S[3] = -GRAVITY * Q[2];
253fv_solver = exahype2.solvers.fv.godunov.GlobalFixedTimeStep(
255 patch_size=args.degrees_of_freedom,
256 unknowns={
"rho": 1,
"rhoU": 1,
"rhoV": 1,
"rhoE": 1},
257 auxiliary_variables=0,
260 normalised_time_step_size=0.001,
263fv_solver.set_implementation(
264 initial_conditions=initial_conditions,
265 boundary_conditions=boundary_conditions,
266 max_eigenvalue=max_eigenvalue,
268 source_term=source_term,
271project.add_solver(fv_solver)
286tracer_particles = fv_solver.add_tracer(
287 name=
"Tracers", particle_attributes=tracer_attributes
291 file_path=
"InitTracers.dat",
299init_tracers = exahype2.tracer.InsertParticlesFromFile(
300 particle_set=tracer_particles,
301 filename=
"InitTracers.dat",
305init_tracers.descend_invocation_order = 0
306project.add_action_set_to_initialisation(init_tracers)
308tracing_action_set = exahype2.tracer.FiniteVolumesTracingWithForce(
312 integration_scheme=integration_schemes[args.integration],
313 interpolation_scheme=interpolation_methods[args.interpolation],
316tracing_action_set.descend_invocation_order = (
317 fv_solver._action_set_update_patch.descend_invocation_order + 1
320project.add_action_set_to_timestepping(tracing_action_set)
321project.add_action_set_to_initialisation(tracing_action_set)
323project.set_load_balancer(
"new ::exahype2::LoadBalancingConfiguration")
324project.set_build_mode(mode=peano4.output.string_to_mode(args.build_mode))
325project = project.generate_Peano4_project(verbose=
False)
326project.output.makefile.add_CXX_flag(
"-DGAMMA=1.4")
327project.output.makefile.add_CXX_flag(
"-DGRAVITY=2.0")
328project.output.makefile.add_CXX_flag(
"-DP0=1.0")
329project.output.makefile.add_CXX_flag(
"-DRHO_AMBIENT=1.0")
330project.output.makefile.add_CXX_flag(
"-DRHO_HOT=0.05")
331project.build(make=
True, make_clean_first=
True, throw_away_data_after_build=
True)
generate_particles_in_plume(str file_path="InitTracers.dat", int num_x=8, int num_y=8, float x_min=0.46, float x_max=0.54, float y_min=0.11, float y_max=0.29, int round_decimals=6)
Generates a .dat file with particles on a uniform grid inside a rectangular plume.