Peano
Loading...
Searching...
No Matches
fv-tracers-volcanic-plume-rise.py
Go to the documentation of this file.
1# This file is part of the ExaHyPE2 project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3import peano4
4import exahype2
5
6"""
7This is a particle tracing simulation for volcanic plume rise using the Euler equations.
8
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.
12"""
13
14
16 file_path: str = "InitTracers.dat",
17 num_x: int = 8,
18 num_y: int = 8,
19 x_min: float = 0.46,
20 x_max: float = 0.54,
21 y_min: float = 0.11,
22 y_max: float = 0.29,
23 round_decimals: int = 6,
24):
25 """
26 Generates a .dat file with particles on a uniform grid inside a rectangular plume.
27
28 Parameters:
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.
37 """
38 if num_x < 1 or num_y < 1:
39 raise ValueError("num_x and num_y must be >= 1")
40 if x_min > x_max:
41 raise ValueError("x_min must be <= x_max")
42 if y_min > y_max:
43 raise ValueError("y_min must be <= y_max")
44
45 if num_x == 1:
46 x_values = [round((x_min + x_max) / 2.0, round_decimals)]
47 else:
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)]
50
51 if num_y == 1:
52 y_values = [round((y_min + y_max) / 2.0, round_decimals)]
53 else:
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)]
56
57 with open(file_path, "w") as file:
58 for x in x_values:
59 for y in y_values:
60 file.write(f"{x:.{round_decimals}f} {y:.{round_decimals}f}\n")
61
62 total = num_x * num_y
63 print(f"Generated {total} particles inside plume region and saved to {file_path}")
64
65
66parser = exahype2.ArgumentParser(
67 "ExaHyPE 2 - Finite Volumes Particle Tracing Testing Script"
68)
69
70interpolation_methods = {
71 "None": 0, # No interpolation (use raw values)
72 "Constant": 1, # Piecewise constant interpolation (nearest neighbor)
73 "Linear": 2, # Piecewise linear interpolation (more accurate)
74}
75parser.add_argument(
76 "-interp",
77 "--interpolation",
78 default="Constant",
79 help="|".join(interpolation_methods),
80)
81
82integration_schemes = {
83 "Static": 0, # No integration (static particles)
84 "Euler": 1, # Explicit Euler method (simple but less accurate)
85 "SemiEuler": 2, # Semi-implicit Euler method (better stability)
86 "Verlet": 3, # Velocity Verlet algorithm (good for orbital mechanics)
87 "Midpoint": 4, # Midpoint method (2nd order Runge-Kutta)
88 "RK3": 5, # 3rd order Runge-Kutta method (high accuracy)
89 "RK4": 6, # 4th order Runge-Kutta method (higher accuracy)
90}
91parser.add_argument(
92 "-integ", "--integration", default="RK4", help="|".join(integration_schemes)
93)
94
95parser.set_defaults(
96 min_depth=3,
97 degrees_of_freedom=64,
98 end_time=3.0,
99)
100
101args = parser.parse_args()
102
103size = [1.0, 1.0]
104offset = [0.0, 0.0]
105max_h = 1.1 * min(size) / (3.0**args.min_depth)
106min_h = max_h * 3.0 ** (-args.amr_levels)
107
108project = exahype2.Project(
109 namespace=["tests", "exahype2", "fv"],
110 project_name=".",
111 directory=".",
112 executable="ExaHyPE",
113)
114
115if args.number_of_snapshots <= 0:
116 time_in_between_plots = 0.0
117else:
118 time_in_between_plots = args.end_time / args.number_of_snapshots
119 project.set_output_path(args.output)
120
121project.set_global_simulation_parameters(
122 dimensions=2,
123 size=size,
124 offset=offset,
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,
129 periodic_BC=[
130 args.periodic_boundary_conditions_x,
131 args.periodic_boundary_conditions_y,
132 ],
133)
134
135initial_conditions = """
136 // Parameters for volcanic plume simulation using defined constants
137 Q[4] = 0.0; // Initialise plume indicator
138
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;
143
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);
148
149 bool in_plume = in_plume_column &&
150 (x(1) >= plume_base_y) &&
151 (x(1) <= plume_top_y);
152
153 // Compute piecewise hydrostatic pressure
154 double p_local = 0.0;
155
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
162 p_local = P0 +
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
166 } else {
167 // Inside plume -> partial light column
168 p_local = P0 +
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
171 Q[4] = 1.0;
172 }
173 } else {
174 // Outside plume column -> pure cold fluid
175 p_local = P0 + GRAVITY * (DomainSize[1] - x(1)) * RHO_AMBIENT;
176 }
177
178 // Energy from pressure
179 double e_local = p_local / (GAMMA - 1.0);
180
181 Q[0] = in_plume ? RHO_HOT : RHO_AMBIENT; // density
182 Q[1] = 0.0; // rho*u
183 Q[2] = 0.0; // rho*v
184 Q[3] = e_local; // total energy
185"""
186
187boundary_conditions = """
188 if (x(1) >= DomainSize[1] - 0.001) {
189 // Top boundary (outflow - zero gradient)
190 Qoutside[0] = RHO_AMBIENT;
191 Qoutside[1] = 0.0;
192 Qoutside[2] = 0.0;
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];
206 }
207"""
208
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;
213
214 const auto uSq = u0 * u0 + u1 * u1;
215 const auto u_n = (normal == 0) ? u0 : u1;
216
217 const auto internalE = Q[Shortcuts::rhoE] - 0.5 * Q[Shortcuts::rho] * uSq;
218 const auto p = (GAMMA - 1.0) * internalE;
219"""
220
221max_eigenvalue = r"""
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));
226 return result;
227""".format(
228 compute_primitive_variables=compute_primitive_variables
229)
230
231flux = r"""
232 {compute_primitive_variables}
233
234 F[Shortcuts::rho] = Q[Shortcuts::rhoU + normal];
235
236 F[Shortcuts::rhoU] = Q[Shortcuts::rhoU] * u_n;
237 F[Shortcuts::rhoV] = Q[Shortcuts::rhoV] * u_n;
238
239 F[Shortcuts::rhoU + normal] += p;
240
241 F[Shortcuts::rhoE] = (Q[Shortcuts::rhoE] + p) * u_n;
242""".format(
243 compute_primitive_variables=compute_primitive_variables
244)
245
246source_term = """
247 S[0] = 0.0;
248 S[1] = 0.0;
249 S[2] = -GRAVITY * Q[0];
250 S[3] = -GRAVITY * Q[2];
251"""
252
253fv_solver = exahype2.solvers.fv.godunov.GlobalFixedTimeStep(
254 name="FVSolver",
255 patch_size=args.degrees_of_freedom,
256 unknowns={"rho": 1, "rhoU": 1, "rhoV": 1, "rhoE": 1},
257 auxiliary_variables=0,
258 min_volume_h=min_h,
259 max_volume_h=max_h,
260 normalised_time_step_size=0.001,
261)
262
263fv_solver.set_implementation(
264 initial_conditions=initial_conditions,
265 boundary_conditions=boundary_conditions,
266 max_eigenvalue=max_eigenvalue,
267 flux=flux,
268 source_term=source_term,
269)
270
271project.add_solver(fv_solver)
272
273tracer_attributes = {
274 "unknowns": {
275 "rho": 1,
276 "fluid_u": 1,
277 "fluid_v": 1,
278 "u": 1,
279 "v": 1,
280 "a_x": 1,
281 "a_y": 1,
282 "d": 1,
283 }
284}
285
286tracer_particles = fv_solver.add_tracer(
287 name="Tracers", particle_attributes=tracer_attributes
288)
289
291 file_path="InitTracers.dat",
292 num_x=8,
293 num_y=8,
294 x_min=0.46,
295 x_max=0.54,
296 y_min=0.11,
297 y_max=0.29,
298)
299init_tracers = exahype2.tracer.InsertParticlesFromFile(
300 particle_set=tracer_particles,
301 filename="InitTracers.dat",
302 scale_factor=1.0,
303)
304
305init_tracers.descend_invocation_order = 0
306project.add_action_set_to_initialisation(init_tracers)
307
308tracing_action_set = exahype2.tracer.FiniteVolumesTracingWithForce(
309 tracer_particles,
310 fv_solver,
311 project,
312 integration_scheme=integration_schemes[args.integration],
313 interpolation_scheme=interpolation_methods[args.interpolation],
314)
315
316tracing_action_set.descend_invocation_order = (
317 fv_solver._action_set_update_patch.descend_invocation_order + 1
318)
319
320project.add_action_set_to_timestepping(tracing_action_set)
321project.add_action_set_to_initialisation(tracing_action_set)
322
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.