tilupy 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
tilupy/analytic_sol.py ADDED
@@ -0,0 +1,2403 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import matplotlib.cm as cm
6
+ import matplotlib
7
+
8
+ from scipy.optimize import fsolve
9
+
10
+ from abc import ABC, abstractmethod
11
+
12
+
13
+ class Depth_result(ABC):
14
+ """Abstract base class representing simulation results for flow depth and velocity.
15
+
16
+ This class defines a common interface for analytical solution that compute flow height
17
+ h(x,t) and flow velocity u(x,t).
18
+
19
+ Parameters
20
+ ----------
21
+ theta : float, optional
22
+ Angle of the surface, in radian, by default 0.
23
+
24
+ Attributes
25
+ ----------
26
+ _g = 9.81 : float
27
+ Gravitational constant.
28
+ _theta : float
29
+ Angle of the surface, in radian.
30
+ _x : float or np.ndarray
31
+ Spatial coordinates.
32
+ _t : float or np.ndarray
33
+ Time instant.
34
+ _h : np.ndarray
35
+ Flow height depending on space at a moment.
36
+ _u : np.ndarray
37
+ Flow velocity depending on space at a moment.
38
+ """
39
+ def __init__(self,
40
+ theta: float=None
41
+ ):
42
+ self._g = 9.81
43
+ self._theta = theta
44
+ self._x = None
45
+ self._t = None
46
+ self._h = None
47
+ self._u = None
48
+
49
+
50
+ @abstractmethod
51
+ def compute_h(self,
52
+ x: float | np.ndarray,
53
+ t: float | np.ndarray
54
+ ) -> None:
55
+ """Virtual function that compute the flow height :attr:`_h` at given space and time.
56
+
57
+ Parameters
58
+ ----------
59
+ x : float or np.ndarray
60
+ Spatial coordinates.
61
+ t : float or np.ndarray
62
+ Time instant.
63
+ """
64
+ pass
65
+
66
+
67
+ @abstractmethod
68
+ def compute_u(self,
69
+ x: float | np.ndarray,
70
+ t: float | np.ndarray
71
+ ) -> None:
72
+ """Virtual function that compute the flow velocity :attr:`_u` at given space and time.
73
+
74
+ Parameters
75
+ ----------
76
+ x : float or np.ndarray
77
+ Spatial coordinates.
78
+ t : float or np.ndarray
79
+ Time instant.
80
+ """
81
+ pass
82
+
83
+
84
+ @property
85
+ def h(self):
86
+ """Accessor of h(x,t) solution.
87
+
88
+ Returns
89
+ -------
90
+ numpy.ndarray
91
+ Attribute :attr:`_h`. If None, no solution computed.
92
+ """
93
+ return self._h
94
+
95
+
96
+ @property
97
+ def u(self):
98
+ """Accessor of u(x,t) solution.
99
+
100
+ Returns
101
+ -------
102
+ numpy.ndarray
103
+ Attribute :attr:`_u`. If None, no solution computed.
104
+ """
105
+ return self._u
106
+
107
+
108
+ @property
109
+ def x(self):
110
+ """Accessor of the spatial distribution of the computed solution.
111
+
112
+ Returns
113
+ -------
114
+ numpy.ndarray
115
+ Attribute :attr:`_x`. If None, no solution computed.
116
+ """
117
+ return self._x
118
+
119
+
120
+ @property
121
+ def t(self):
122
+ """Accessor of the time instant of the computed solution.
123
+
124
+ Returns
125
+ -------
126
+ float or numpy.ndarray
127
+ Attribut :attr:`_t`. If None, no solution computed.
128
+ """
129
+ return self._t
130
+
131
+
132
+ def plot(self,
133
+ show_h: bool=False,
134
+ show_u: bool=False,
135
+ show_surface: bool=False,
136
+ linestyles: list[str]=None,
137
+ x_unit:str = "m",
138
+ h_unit:str = "m",
139
+ u_unit:str = "m/s",
140
+ show_plot:bool = True,
141
+ figsize:tuple = None,
142
+ ) -> matplotlib.axes._axes.Axes:
143
+ """Plot the simulation results.
144
+
145
+ Parameters
146
+ ----------
147
+ show_h : bool, optional
148
+ If True, plot the flow height (:attr:`_h`) curve.
149
+ show_u : bool, optional
150
+ If True, plot the flow velocity (:attr:`_u`) curve.
151
+ show_surface : bool, optional
152
+ If True, plot the slop of the surface.
153
+ linestyles : list[str], optional
154
+ List of linestyle to applie to the graph, must have the same since as the numbre of curve to plot or it
155
+ will not be taken into account (-1), by default None. If None, copper colormap will be applied.
156
+ x_unit: str
157
+ Space unit.
158
+ h_unit: str
159
+ Height unit.
160
+ u_unit: str
161
+ Velocity unit.
162
+ show_plot: bool, optional
163
+ If True, show the resulting plot. By default True.
164
+ figsize: tuple, optional
165
+ Size of the wanted plot, by default None.
166
+
167
+ Return
168
+ ------
169
+ matplotlib.axes._axes.Axes
170
+ Resulting plot.
171
+
172
+ Raises
173
+ ------
174
+ ValueError
175
+ If no solution computed (:attr:`_h` and :attr:`_u` are None).
176
+ """
177
+ z_surf = [0, 0]
178
+
179
+ if self._h is None and self._u is None:
180
+ raise ValueError("No solution computed.")
181
+
182
+ fig, ax = plt.subplots(figsize=figsize)
183
+
184
+ if show_h and self._h is not None:
185
+ if self._h.ndim == 1:
186
+ ax.plot(self._x, self._h, color='black', linewidth=1)
187
+ else:
188
+ if linestyles is None or len(linestyles)!=(len(self._t)):
189
+ norm = plt.Normalize(vmin=min(self._t), vmax=max(self._t))
190
+ cmap = plt.cm.copper
191
+
192
+ for h_idx, h_val in enumerate(self._h):
193
+ t_val = self._t[h_idx]
194
+ if linestyles is None or len(linestyles)!=(len(self._t)):
195
+ color = cmap(norm(t_val)) if t_val != 0 else "red"
196
+ l_style = "-" if t_val != 0 else (0, (1, 4))
197
+ else:
198
+ color = "black" if t_val != 0 else "red"
199
+ l_style = linestyles[h_idx] if t_val != 0 else (0, (1, 4))
200
+ ax.plot(self._x, h_val, color=color, linestyle=l_style, label=f"t={t_val}s")
201
+
202
+ if show_surface:
203
+ ax.plot([self._x[0], self._x[-1]], z_surf, color='black', linewidth=2)
204
+
205
+ ax.grid(which='major')
206
+ ax.grid(which='minor', alpha=0.5)
207
+ ax.set_xlim(left=min(self._x), right=max(self._x))
208
+
209
+ ax.set_title(f"Flow height for t={self._t}")
210
+ ax.set_xlabel(f"x [{x_unit}]")
211
+ ax.set_ylabel(f"h [{h_unit}]")
212
+ ax.legend(loc='upper right')
213
+ if show_plot:
214
+ plt.show()
215
+
216
+ return ax
217
+
218
+ if show_u and self._u is not None:
219
+ if self._u.ndim == 1:
220
+ ax.plot(self._x, self._u, color='black', linewidth=1)
221
+
222
+ else:
223
+ if linestyles is None or len(linestyles)!=(len(self._t)):
224
+ norm = plt.Normalize(vmin=min(self._t), vmax=max(self._t))
225
+ cmap = plt.cm.copper
226
+
227
+ for u_idx, u_val in enumerate(self._u):
228
+ t_val = self._t[u_idx]
229
+ if t_val == 0:
230
+ continue
231
+ if linestyles is None or len(linestyles)!=(len(self._t)):
232
+ color = cmap(norm(t_val))
233
+ l_style = "-"
234
+ else:
235
+ color = "black"
236
+ l_style = linestyles[u_idx]
237
+ ax.plot(self._x, u_val, color=color, linestyle=l_style, label=f"t={t_val}s")
238
+
239
+ ax.grid(which='major')
240
+ ax.grid(which='minor', alpha=0.5)
241
+ ax.set_xlim(left=min(self._x), right=max(self._x))
242
+
243
+ ax.set_title(f"Flow velocity for t={self._t}")
244
+ ax.set_xlabel(f"x [{x_unit}]")
245
+ ax.set_ylabel(f"u [{u_unit}]")
246
+ ax.legend(loc='best')
247
+ if show_plot:
248
+ plt.show()
249
+
250
+ return ax
251
+
252
+
253
+ class Ritter_dry(Depth_result):
254
+ r"""Dam-break solution on a dry domain using shallow water theory.
255
+
256
+ This class implements the 1D analytical Ritter's solution of an ideal dam break on a dry domain.
257
+ The dam break is instantaneous, over an horizontal and flat surface with no friction.
258
+ It computes the flow height (took verticaly) and velocity over space and time, based on the equation implemanted
259
+ in SWASHES, based on Ritter's equation.
260
+
261
+ Ritter, A., 1892, Die Fortpflanzung der Wasserwellen, Zeitschrift des Vereines Deutscher Ingenieure, vol. 36(33), p. 947-954.
262
+
263
+ Parameters
264
+ ----------
265
+ x_0 : float, optional
266
+ Initial dam location (position along x-axis), by default 0.
267
+ h_0 : float
268
+ Initial water depth to the left of the dam.
269
+
270
+ Attributes
271
+ ----------
272
+ _x0 : float
273
+ Initial dam location (position along x-axis).
274
+ _h0 : float
275
+ Initial water depth to the left of the dam.
276
+ """
277
+ def __init__(self,
278
+ h_0: float,
279
+ x_0: float=0,
280
+ ):
281
+ super().__init__()
282
+ self._x0 = x_0
283
+ self._h0 = h_0
284
+
285
+
286
+ def xa(self, t: float) -> float:
287
+ r"""
288
+ Position of the rarefaction wave front (left-most edge) :
289
+
290
+ .. math::
291
+ x_A(t) = x_0 - t \sqrt{g h_0}
292
+
293
+ Parameters
294
+ ----------
295
+ t : float
296
+ Time instant.
297
+
298
+ Returns
299
+ -------
300
+ float
301
+ Position of the front edge of the rarefaction wave.
302
+ """
303
+ return self._x0 - (t * np.sqrt(self._g*self._h0))
304
+
305
+
306
+ def xb(self, t: float) -> float:
307
+ r"""
308
+ Position of the contact discontinuity:
309
+
310
+ .. math::
311
+ x_B(t) = x_0 + 2 t \sqrt{g h_0}
312
+
313
+ Parameters
314
+ ----------
315
+ t : float
316
+ Time instant.
317
+
318
+ Returns
319
+ -------
320
+ float
321
+ Position of the contact wave (end of rarefaction).
322
+ """
323
+ return self._x0 + (2 * t * np.sqrt(self._g*self._h0))
324
+
325
+
326
+ def compute_h(self,
327
+ x: float | np.ndarray,
328
+ T: float | np.ndarray
329
+ ) -> None:
330
+ r"""Compute the flow height h(x, t) at given time and positions.
331
+
332
+ .. math::
333
+ h(x, t) =
334
+ \begin{cases}
335
+ h_0 & \text{if } x \leq x_A(t), \\\\
336
+ \frac{4}{9g} \left( \sqrt{g h_0} - \frac{x - x_0}{2t} \right)^2 & \text{if } x_A(t) < x \leq x_B(t), \\\\
337
+ 0 & \text{if } x_B(t) < x,
338
+ \end{cases}
339
+
340
+ Parameters
341
+ ----------
342
+ x : float or np.ndarray
343
+ Spatial positions.
344
+ T : float or nd.ndarray
345
+ Time instant.
346
+
347
+ Notes
348
+ -----
349
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._h`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
350
+ """
351
+ if isinstance(x, float):
352
+ x = [x]
353
+ if isinstance(T, float):
354
+ T = [T]
355
+
356
+ self._x = x
357
+ self._t = T
358
+
359
+ h = []
360
+ for t in T:
361
+ sub_h = []
362
+ for i in x:
363
+ if i <= self.xa(t):
364
+ sub_h.append(self._h0)
365
+ elif self.xa(t) < i <= self.xb(t):
366
+ sub_h.append((4/(9*self._g)) *
367
+ (np.sqrt(self._g*self._h0)-((i-self._x0)/(2*t)))**2)
368
+ else:
369
+ sub_h.append(0)
370
+ h.append(sub_h)
371
+ self._h = np.array(h)
372
+
373
+
374
+ def compute_u(self,
375
+ x: float | np.ndarray,
376
+ T: float | np.ndarray
377
+ ) -> None:
378
+ r"""Compute the flow velocity u(x, t) at given time and positions.
379
+
380
+ .. math::
381
+ u(x,t) =
382
+ \begin{cases}
383
+ 0 & \text{if } x \leq x_A(t), \\\\
384
+ \frac{2}{3} \left( \frac{x - x_0}{t} + \sqrt{g h_0} \right) & \text{if } x_A(t) < x \leq x_B(t), \\\\
385
+ 0 & \text{if } x_B(t) < x,
386
+ \end{cases}
387
+
388
+ Parameters
389
+ ----------
390
+ x : float or np.ndarray
391
+ Spatial positions.
392
+ T : float or np.ndarray
393
+ Time instant.
394
+
395
+ Notes
396
+ -----
397
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._u`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
398
+ """
399
+ if isinstance(x, float):
400
+ x = [x]
401
+ if isinstance(T, float):
402
+ T = [T]
403
+
404
+ self._x = x
405
+ self._t = T
406
+
407
+ u = []
408
+ for t in T:
409
+ sub_u = []
410
+ for i in x:
411
+ if i <= self.xa(t):
412
+ sub_u.append(np.nan)
413
+ elif i > self.xa(t) and i <= self.xb(t):
414
+ sub_u.append((2/3)*(((i-self._x0)/t) + np.sqrt(self._g*self._h0)))
415
+ else:
416
+ sub_u.append(np.nan)
417
+ u.append(sub_u)
418
+ self._u = np.array(u)
419
+
420
+
421
+ class Stoker_SWASHES_wet(Depth_result):
422
+ r"""Dam-break solution on a wet domain using shallow water theory.
423
+
424
+ This class implements the 1D analytical Stoker's solution of an ideal dam break on a wet domain.
425
+ The dam break is instantaneous, over an horizontal and flat surface with no friction.
426
+ It computes the flow height (took verticaly) and velocity over space and time, based on the equation implemanted
427
+ in SWASHES, based on Stoker's equation.
428
+
429
+ Delestre, O., Lucas, C., Ksinant, P.-A., Darboux, F., Laguerre, C., Vo, T.-N.-T., James, F. & Cordier, S., 2013, SWASHES: a compilation of shallow water
430
+ analytic solutions for hydraulic and environmental studies, International Journal for Numerical Methods in Fluids, v. 72(3), p. 269-300, doi:10.1002/fld.3741.
431
+
432
+ Stoker, J.J., 1957, Water Waves: The Mathematical Theory with Applications, Pure and Applied Mathematics, vol. 4, Interscience Publishers, New York, USA.
433
+
434
+ Parameters
435
+ ----------
436
+ x_0 : float
437
+ Initial dam location (position along x-axis).
438
+ h_0 : float
439
+ Water depth to the left of the dam.
440
+ h_r : float
441
+ Water depth to the right of the dam.
442
+ h_m : float, optional
443
+ Intermediate height used to compute the critical speed cm. If not provided,
444
+ it will be computed numerically via the 'compute_cm()' method.
445
+
446
+ Attributes
447
+ ----------
448
+ _x0 : float
449
+ Initial dam location (position along x-axis).
450
+ _h0 : float
451
+ Water depth to the left of the dam.
452
+ _hr : float
453
+ Water depth to the right of the dam.
454
+ _cm : float
455
+ Critical velocity.
456
+ """
457
+ def __init__(self,
458
+ x_0: float,
459
+ h_0: float,
460
+ h_r: float,
461
+ h_m: float=None
462
+ ):
463
+ super().__init__()
464
+ self._x0 = x_0
465
+ self._h0 = h_0
466
+ self._hr = h_r
467
+ self._cm = None
468
+ self.compute_cm()
469
+
470
+ if h_m is not None:
471
+ self._cm = np.sqrt(self._g * h_m)
472
+
473
+
474
+ def xa(self, t: float) -> float:
475
+ r"""
476
+ Position of the rarefaction wave front (left-most edge) :
477
+
478
+ .. math::
479
+ x_A(t) = x_0 - t \sqrt{g h_0}
480
+
481
+ Parameters
482
+ ----------
483
+ t : float
484
+ Time instant.
485
+
486
+ Returns
487
+ -------
488
+ float
489
+ Position of the front edge of the rarefaction wave.
490
+ """
491
+ return self._x0 - (t * np.sqrt(self._g*self._h0))
492
+
493
+
494
+ def xb(self, t: float) -> float:
495
+ r"""
496
+ Position of the contact discontinuity:
497
+
498
+ .. math::
499
+ x_B(t) = x_0 + t \left( 2 \sqrt{g h_0} - 3 c_m \right)
500
+
501
+ Parameters
502
+ ----------
503
+ t : float
504
+ Time instant.
505
+
506
+ Returns
507
+ -------
508
+ float
509
+ Position of the contact wave (end of rarefaction).
510
+ """
511
+ return self._x0 + (t * ((2 * np.sqrt(self._g*self._h0)) - (3*self._cm)))
512
+
513
+
514
+ def xc(self, t: float) -> float:
515
+ r"""
516
+ Position of the shock wave front (right-most wave):
517
+
518
+ .. math::
519
+ x_C(t) = x_0 + t \cdot \frac{2 c_m^2 \left( \sqrt{g h_0} - c_m \right)}{c_m^2 - g h_r}
520
+
521
+ Parameters
522
+ ----------
523
+ t : float
524
+ Time instant.
525
+
526
+ Returns
527
+ -------
528
+ float
529
+ Position of the shock front.
530
+ """
531
+ return self._x0 + (t * (((2*self._cm**2)*(np.sqrt(self._g*self._h0)-self._cm)) / ((self._cm**2) - (self._g*self._hr))))
532
+
533
+
534
+ def equation_cm(self, cm) -> float:
535
+ r"""Equation of the critical velocity cm:
536
+
537
+ .. math::
538
+ -8.g.hr.cm^{2}.(g.h0 - cm^{2})^{2} + (cm^{2} - g.hr)^{2} . (cm^{2} + g.hr) = 0
539
+
540
+ Parameters
541
+ ----------
542
+ cm : float
543
+ Trial value for :attr:`_cm`.
544
+
545
+ Returns
546
+ -------
547
+ float
548
+ Residual of the equation. Zero when :attr:`_cm` satisfies the system.
549
+ """
550
+ return -8 * self._g * self._hr * cm**2 * (self._g * self._h0 - cm**2)**2 + (cm**2 - self._g * self._hr)**2 * (cm**2 + self._g * self._hr)
551
+
552
+
553
+ def compute_cm(self) -> None:
554
+ r"""Solves the non-linear equation to compute the critical velocity :attr:`_cm`.
555
+
556
+ Uses numerical root-finding to find a valid value of cm that separates
557
+ the flow regimes. Sets :attr:`_cm` if a valid solution is found.
558
+ """
559
+ guesses = np.linspace(0.01, 1000, 1000)
560
+ solutions = []
561
+
562
+ for guess in guesses:
563
+ sol = fsolve(self.equation_cm, guess)[0]
564
+
565
+ if abs(self.equation_cm(sol)) < 1e-6 and not any(np.isclose(sol, s, atol=1e-6) for s in solutions):
566
+ solutions.append(sol)
567
+
568
+ for sol in solutions:
569
+ hm = sol**2 / self._g
570
+ if hm < self._h0 and hm > self._hr:
571
+ find = True
572
+ self._cm = sol
573
+ break
574
+ else:
575
+ find = False
576
+
577
+ if find:
578
+ print(f"Find cm: {self._cm}\nhm:{self._cm**2 / self._g}")
579
+ else:
580
+ print(
581
+ f"Didn't find cm, try with greater range of value. Default value: {None}")
582
+
583
+
584
+ def compute_h(self,
585
+ x: float | np.ndarray,
586
+ T: float | np.ndarray
587
+ ) -> None:
588
+ r"""Compute the flow height h(x, t) at given time and positions.
589
+
590
+ .. math::
591
+ h(x, t) =
592
+ \begin{cases}
593
+ h_0 & \text{if } x \leq x_A(t), \\\\
594
+ \frac{4}{9g} \left( \sqrt{g h_0} - \frac{x - x_0}{2t} \right)^2 & \text{if } x_A(t) < x \leq x_B(t), \\\\
595
+ \frac{c_m^2}{g} & \text{if } x_B(t) < x \leq x_C(t), \\\\
596
+ h_r & \text{if } x_C(t) < x,
597
+ \end{cases}
598
+
599
+ Parameters
600
+ ----------
601
+ x : float or np.ndarray
602
+ Spatial positions.
603
+ T : float or np.ndarray
604
+ Time instant.
605
+
606
+ Notes
607
+ -----
608
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._h`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
609
+ """
610
+ if self._cm is not None:
611
+ if isinstance(x, float):
612
+ x = [x]
613
+ self._x = x
614
+ self._t = T
615
+
616
+ if isinstance(T, float):
617
+ h = []
618
+ for i in x:
619
+ if i <= self.xa(T):
620
+ h.append(self._h0)
621
+ # elif i > self.xa(t) and i <= self.xb(t):
622
+ elif self.xa(T) < i <= self.xb(T):
623
+ h.append((4/(9*self._g))*(np.sqrt(self._g *
624
+ self._h0)-((i-self._x0)/(2*T)))**2) # i-x0 and not i to recenter the breach of the dam at x=0.
625
+ elif self.xb(T) < i <= self.xc(T):
626
+ h.append((self._cm**2)/self._g)
627
+ else:
628
+ h.append(self._hr)
629
+ self._h = np.array(h)
630
+ else:
631
+ h = []
632
+ for t in T:
633
+ sub_h = []
634
+ for i in x:
635
+ if i <= self.xa(t):
636
+ sub_h.append(self._h0)
637
+ elif self.xa(t) < i <= self.xb(t):
638
+ sub_h.append((4/(9*self._g))*(np.sqrt(self._g *
639
+ self._h0)-((i-self._x0)/(2*t)))**2)
640
+ elif self.xb(t) < i <= self.xc(t):
641
+ sub_h.append((self._cm**2)/self._g)
642
+ else:
643
+ sub_h.append(self._hr)
644
+ h.append(sub_h)
645
+ self._h = np.array(h)
646
+
647
+ else:
648
+ print("No critical velocity found")
649
+
650
+
651
+ def compute_u(self,
652
+ x: float | np.ndarray,
653
+ T: float | np.ndarray
654
+ ) -> None:
655
+ r"""Compute the flow velocity u(x, t) at given time and positions.
656
+
657
+ .. math::
658
+ u(x,t) =
659
+ \begin{cases}
660
+ 0 & \text{if } x \leq x_A(t), \\\\
661
+ \frac{2}{3} \left( \frac{x - x_0}{t} + \sqrt{g h_0} \right) & \text{if } x_A(t) < x \leq x_B(t), \\\\
662
+ 2 \left( \sqrt{g h_0} - c_m \right) & \text{if } x_B(t) < x \leq x_C(t), \\\\
663
+ 0 & \text{if } x_C(t) < x,
664
+ \end{cases}
665
+
666
+ Parameters
667
+ ----------
668
+ x : float or np.ndarray
669
+ Spatial positions.
670
+ T : float or np.ndarray
671
+ Time instant.
672
+
673
+ Notes
674
+ -----
675
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._u`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
676
+ """
677
+ if self._cm is not None:
678
+ if isinstance(x, float):
679
+ x = [x]
680
+ self._x = x
681
+ self._t = T
682
+
683
+ if isinstance(T, float):
684
+ u = []
685
+ for i in x:
686
+ if i <= self.xa(T):
687
+ u.append(0)
688
+ elif i > self.xa(T) and i <= self.xb(T):
689
+ u.append((2/3)*(((i-self._x0)/T) +
690
+ np.sqrt(self._g*self._h0)))
691
+ elif i > self.xb(T) and i <= self.xc(T):
692
+ u.append(2*(np.sqrt(self._g*self._h0) - self._cm))
693
+ else:
694
+ u.append(0)
695
+ self._u = np.array(u)
696
+
697
+ else:
698
+ u = []
699
+ for t in T:
700
+ sub_u = []
701
+ for i in x:
702
+ if i <= self.xa(t):
703
+ sub_u.append(0)
704
+ elif i > self.xa(t) and i <= self.xb(t):
705
+ sub_u.append((2/3)*(((i-self._x0)/t) +
706
+ np.sqrt(self._g*self._h0)))
707
+ elif i > self.xb(t) and i <= self.xc(t):
708
+ sub_u.append(2*(np.sqrt(self._g*self._h0) - self._cm))
709
+ else:
710
+ sub_u.append(0)
711
+ u.append(sub_u)
712
+ self._u = np.array(u)
713
+
714
+ else:
715
+ print("First define cm")
716
+
717
+
718
+ class Stoker_SARKHOSH_wet(Depth_result):
719
+ r"""Dam-break solution on a wet domain using shallow water theory.
720
+
721
+ This class implements the 1D analytical Stoker's solution of an ideal dam break on a wet domain.
722
+ The dam break is instantaneous, over an horizontal and flat surface with no friction.
723
+ It computes the flow height (took verticaly) and velocity over space and time, based on the equation implemanted
724
+ in SWASHES, based on Stoker's equation.
725
+
726
+ Sarkhosh, P., 2021, Stoker solution package, version 1.0.0, Zenodo. https://doi.org/10.5281/zenodo.5598374
727
+
728
+ Stoker, J.J., 1957, Water Waves: The Mathematical Theory with Applications, Pure and Applied Mathematics, vol. 4, Interscience Publishers, New York, USA.
729
+
730
+ Parameters
731
+ ----------
732
+ h_0 : float
733
+ Initial water depth to the left of the dam.
734
+ h_r : float
735
+ Initial water depth to the right of the dam.
736
+
737
+ Attributes
738
+ ----------
739
+ _h0 : float
740
+ Water depth to the left of the dam.
741
+ _hr : float
742
+ Water depth to the right of the dam.
743
+ _cm : float
744
+ Shock front speed.
745
+ _hm : float
746
+ Height of the shock front.
747
+ """
748
+ def __init__(self,
749
+ h_0: float,
750
+ h_r: float,
751
+ ):
752
+ super().__init__()
753
+ self._h0 = h_0
754
+ self._hr = h_r
755
+
756
+ if self._hr == 0:
757
+ self._cm = 0
758
+ self._hm = 0
759
+ else:
760
+ self._cm = self.compute_cm()
761
+ self._hm = 0.5 * self._hr * (np.sqrt(1 + 8 * self._cm**2 / np.sqrt(self._g * self._hr)**2) - 1)
762
+
763
+
764
+ def xa(self, t: float) -> float:
765
+ r"""
766
+ Position of the rarefaction wave front (left-most edge) :
767
+
768
+ .. math::
769
+ x_A(t) = x_0 - t \sqrt{g h_0}
770
+
771
+ Parameters
772
+ ----------
773
+ t : float
774
+ Time instant.
775
+
776
+ Returns
777
+ -------
778
+ float
779
+ Position of the front edge of the rarefaction wave.
780
+ """
781
+ return -(t * np.sqrt(self._g*self._h0))
782
+
783
+
784
+ def xb(self, hm: float, t: float) -> float:
785
+ r"""
786
+ Position of the contact discontinuity:
787
+
788
+ .. math::
789
+ x_B(t) = t \left( 2 \sqrt{g h_0} - 3 \sqrt{g h_m} \right)
790
+
791
+ Parameters
792
+ ----------
793
+ hm : float
794
+ Height of the shock front.
795
+ t : float
796
+ Time instant.
797
+
798
+ Returns
799
+ -------
800
+ float
801
+ Position of the contact wave (end of rarefaction).
802
+ """
803
+ return (2 * np.sqrt(self._g * self._h0) - 3 * np.sqrt(self._g * hm)) * t
804
+
805
+
806
+ def xc(self, cm: float, t: float) -> float:
807
+ r"""
808
+ Position of the shock wave front (right-most wave):
809
+
810
+ .. math::
811
+ x_C(t) = c_m t
812
+
813
+ Parameters
814
+ ----------
815
+ cm : float
816
+ Shock front speed.
817
+ t : float
818
+ Time instant.
819
+
820
+ Returns
821
+ -------
822
+ float
823
+ Position of the shock front.
824
+ """
825
+ return cm * t
826
+
827
+
828
+ def compute_cm(self) -> float:
829
+ r"""Compute the shock front speed using Newton-Raphson's method to find the solution of:
830
+
831
+ .. math::
832
+ c_m h_r - h_r \left( \sqrt{1 + \frac{8 c_m^2}{g h_r}} - 1 \right) \left( \frac{c_m}{2} - \sqrt{g h_0} + \sqrt{\frac{g h_r}{2} \left( \sqrt{1 + \frac{8 c_m^2}{g h_r}} - 1 \right)} \right) = 0
833
+
834
+ Returns
835
+ -------
836
+ float
837
+ Speed of the shock front.
838
+ """
839
+ f_cm = 1
840
+ df_cm = 1
841
+ cm = 10 * self._h0
842
+
843
+ while abs(f_cm / cm) > 1e-10:
844
+ root_term = np.sqrt(8 * cm**2 / np.sqrt(self._g * self._hr)**2 + 1)
845
+ inner_sqrt = np.sqrt(self._g * self._hr * (root_term - 1) / 2)
846
+
847
+ f_cm = cm * self._hr - self._hr * (root_term - 1) * (cm / 2 - np.sqrt(self._g * self._h0) + inner_sqrt)
848
+ df_cm = (self._hr
849
+ - self._hr * ((2 * cm * self._g * self._hr) / (np.sqrt(self._g * self._hr)**2 * root_term * inner_sqrt) + 0.5) * (root_term - 1)
850
+ - (8 * cm * self._hr * (cm / 2 - np.sqrt(self._g * self._h0) + inner_sqrt)) / (np.sqrt(self._g * self._hr)**2 * root_term))
851
+
852
+ cm -= f_cm / df_cm
853
+
854
+ return cm
855
+
856
+
857
+ def compute_h(self,
858
+ x: float | np.ndarray,
859
+ T: float | np.ndarray
860
+ ) -> None:
861
+ r"""Compute the flow height h(x, t) at given time and positions.
862
+
863
+ .. math::
864
+ h(x, t) =
865
+ \begin{cases}
866
+ h_0 & \text{if } x \leq x_A(t), \\\\
867
+ \frac{\left( 2 \sqrt{g h_0} - \frac{x}{t} \right)^2}{9 g} & \text{if } x_A(t) < x \leq x_B(t), \\\\
868
+ h_m = \frac{1}{2} h_r \left( \sqrt{1 + \frac{8 c_m^2}{g h_r}} - 1 \right) & \text{if } x_B(t) < x \leq x_C(t), \\\\
869
+ h_r & \text{if } x_C(t) < x
870
+ \end{cases}
871
+
872
+ Parameters
873
+ ----------
874
+ x : float or np.ndarray
875
+ Spatial positions.
876
+ T : float or np.ndarray
877
+ Time instant.
878
+
879
+ Notes
880
+ -----
881
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._h`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
882
+ """
883
+ if isinstance(x, float):
884
+ x = [x]
885
+ if isinstance(T, float):
886
+ T = [T]
887
+
888
+ self._x = x
889
+ self._t = T
890
+
891
+ h = []
892
+ for t in T:
893
+ sub_h = []
894
+ for i in x:
895
+ if t == 0:
896
+ h_val = (2 * np.sqrt(self._g * self._h0) - 1e18) ** 2 / (9 * self._g)
897
+ else:
898
+ h_val = (2 * np.sqrt(self._g * self._h0) - (i/t)) ** 2 / (9 * self._g)
899
+
900
+ if i < self.xa(t):
901
+ # if h_val >= self._h0:
902
+ h_val = self._h0
903
+
904
+ if self._hm == 0 and h_val > sub_h[-1]:
905
+ h_val = 0
906
+ else:
907
+ if (self.xb(self._hm, t) < i <= self.xc(self._cm, t)) and h_val <= self._hm:
908
+ h_val = self._hm
909
+ elif i > self.xc(self._cm, t):
910
+ h_val = self._hr
911
+
912
+ sub_h.append(h_val)
913
+ h.append(sub_h)
914
+ self._h = np.array(h)
915
+
916
+
917
+ def compute_u(self,
918
+ x: float | np.ndarray,
919
+ T: float | np.ndarray
920
+ ) -> None:
921
+ r"""Compute the flow velocity u(x, t) at given time and positions.
922
+
923
+ .. math::
924
+ u(x,t) =
925
+ \begin{cases}
926
+ 0 & \text{if } x \leq x_A(t), \\\\
927
+ \frac{2}{3} \left( \frac{x}{t} + \sqrt{g h_0} \right) & \text{if } x_A(t) < x \leq x_B(t), \\\\
928
+ 2 \sqrt{g h_0} - 2 \sqrt{g h_m} & \text{if } x_B(t) < x \leq x_C(t), \\\\
929
+ 0 & \text{if } x_C(t) < x,
930
+ \end{cases}
931
+
932
+ Parameters
933
+ ----------
934
+ x : float or np.ndarray
935
+ Spatial positions.
936
+ T : float or np.ndarray
937
+ Time instant.
938
+
939
+ Notes
940
+ -----
941
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._u`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
942
+ """
943
+ if isinstance(x, float):
944
+ x = [x]
945
+ if isinstance(T, float):
946
+ T = [T]
947
+
948
+ self._x = x
949
+ self._t = T
950
+
951
+ um = 2 * np.sqrt(self._g * self._h0) - 2 * np.sqrt(self._g * self._hm)
952
+
953
+ u = []
954
+ for t in T:
955
+ sub_u = []
956
+ for i in x:
957
+ if t == 0:
958
+ u_val = 2 * (1e18 + np.sqrt(self._g * self._h0)) / 3
959
+ else:
960
+ u_val = 2 * ((i/t) + np.sqrt(self._g * self._h0)) / 3
961
+
962
+ if i < self.xa(t):
963
+ # if h_val >= self._h0:
964
+ u_val = np.nan
965
+
966
+ if self._hm == 0 and u_val > sub_u[-1]:
967
+ u_val = np.nan
968
+ else:
969
+ if (self.xb(self._hm, t) < i <= self.xc(self._cm, t)):
970
+ u_val = um
971
+ elif i > self.xc(self._cm, t):
972
+ u_val = np.nan
973
+
974
+ sub_u.append(u_val)
975
+ u.append(sub_u)
976
+ self._u = np.array(u)
977
+
978
+
979
+ class Mangeney_dry(Depth_result):
980
+ r"""Dam-break solution on an inclined dry domain with friction using shallow water theory.
981
+
982
+ This class implements the 1D analytical Stoker's solution of an ideal dam break on a dry domain.
983
+ The dam break is instantaneous, over an inclined and flat surface with friction.
984
+ It computes the flow height (took normal to the surface) and velocity over space and time with an
985
+ infinitely-long fluid mass on an infinite surface.
986
+
987
+ Mangeney, A., Heinrich, P., & Roche, R., 2000, Analytical solution for testing debris avalanche numerical models,
988
+ Pure and Applied Geophysics, vol. 157, p. 1081-1096.
989
+
990
+ Parameters
991
+ ----------
992
+ x_0 : float
993
+ Initial dam location (position along x-axis), by default 0.
994
+ h_0 : float
995
+ Initial water depth.
996
+ theta : float
997
+ Angle of the surface, in degree.
998
+ delta : float
999
+ Dynamic friction angle (20°-40° for debris avalanche), in degree.
1000
+
1001
+ Attributes
1002
+ ----------
1003
+ _x0 : float
1004
+ Initial dam location (position along x-axis).
1005
+ _h0 : float
1006
+ Initial water depth.
1007
+ _delta : float
1008
+ Dynamic friction angle, in radian.
1009
+ _c0 : float
1010
+ Initial wave propagation speed.
1011
+ _m : float
1012
+ Constant horizontal acceleration of the front.
1013
+ """
1014
+ def __init__(self,
1015
+ x_0: float,
1016
+ h_0: float,
1017
+ theta: float,
1018
+ delta: float,
1019
+ ):
1020
+ super().__init__(theta=np.radians(theta))
1021
+ self._delta = np.radians(delta)
1022
+ self._h0 = h_0
1023
+ self._x0 = x_0
1024
+ self._c0 = self.compute_c0()
1025
+ self._m = self.compute_m()
1026
+
1027
+ # print(f"delta: {self._delta}, theta: {self._theta}, m: {self._m}, c0: {self._c0}")
1028
+
1029
+
1030
+ def xa(self, t: float) -> float:
1031
+ r"""
1032
+ Edge of the quiet area:
1033
+
1034
+ .. math::
1035
+ x_A(t) = x_0 + \frac{1}{2}mt^2 - c_0 t
1036
+
1037
+ Parameters
1038
+ ----------
1039
+ t : float
1040
+ Time instant.
1041
+
1042
+ Returns
1043
+ -------
1044
+ float
1045
+ Position of the edge of the quiet region.
1046
+ """
1047
+ return self._x0 + 0.5*self._m*t**2 - (self._c0*t)
1048
+
1049
+
1050
+ def xb(self, t: float) -> float:
1051
+ r"""
1052
+ Front of the flow:
1053
+
1054
+ .. math::
1055
+ x_B(t) = x_0 + \frac{1}{2}mt^2 + 2 c_0 t
1056
+
1057
+ Parameters
1058
+ ----------
1059
+ t : float
1060
+ Time instant.
1061
+
1062
+ Returns
1063
+ -------
1064
+ float
1065
+ Position of the front edge of the fluid.
1066
+ """
1067
+ return self._x0 + 0.5*self._m*t**2 + (2*self._c0*t)
1068
+
1069
+
1070
+ def compute_c0(self) -> float:
1071
+ r"""Compute the initial wave propagation speed defined by:
1072
+
1073
+ .. math::
1074
+ c_0 = \sqrt{g h_0 \cos{\theta}}
1075
+
1076
+ Returns
1077
+ -------
1078
+ float
1079
+ Value of the initial wave propagation speed.
1080
+ """
1081
+ return np.sqrt(self._g * self._h0 * np.cos(self._theta))
1082
+
1083
+
1084
+ def compute_m(self) -> float:
1085
+ r"""Compute the constant horizontal acceleration of the front defined by:
1086
+
1087
+ .. math::
1088
+ m = g \sin{\theta} - g \cos{\theta} \tan{\delta}
1089
+
1090
+ Returns
1091
+ -------
1092
+ float
1093
+ Value of the constant horizontal acceleration of the front.
1094
+ """
1095
+ return (self._g * np.sin(self._theta)) - (self._g * np.cos(self._theta) * np.tan(self._delta))
1096
+
1097
+
1098
+ def compute_h(self,
1099
+ x: float | np.ndarray,
1100
+ T: float | np.ndarray) -> None:
1101
+ r"""Compute the flow height h(x, t) at given time and positions.
1102
+
1103
+ .. math::
1104
+ h(x, t) =
1105
+ \begin{cases}
1106
+ h_0 & \text{if } x \leq x_A(t), \\\\
1107
+ \frac{1}{9g cos(\theta)} \left(2 c_0 - \frac{x-x_0}{t} + \frac{1}{2} m t \right)^2 & \text{if } x_A(t) < x \leq x_B(t), \\\\
1108
+ 0 & \text{if } x_B(t) < x,
1109
+ \end{cases}
1110
+
1111
+ Parameters
1112
+ ----------
1113
+ x : float or np.ndarray
1114
+ Spatial positions.
1115
+ T : float or np.ndarray
1116
+ Time instant.
1117
+
1118
+ Notes
1119
+ -----
1120
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._h`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
1121
+ """
1122
+ if isinstance(x, float):
1123
+ x = [x]
1124
+ if isinstance(T, float):
1125
+ T = [T]
1126
+
1127
+ self._x = x
1128
+ self._t = T
1129
+
1130
+ h = []
1131
+ for t in T:
1132
+ sub_h = []
1133
+
1134
+ for i in x:
1135
+ if i <= self.xa(t):
1136
+ sub_h.append(self._h0)
1137
+
1138
+ elif self.xa(t) < i < self.xb(t):
1139
+ sub_h.append( (1/(9*self._g*np.cos(self._theta))) * ( (-(i-self._x0)/t) + (2 * self._c0) + (0.5*t*self._m))**2 )
1140
+
1141
+ else:
1142
+ sub_h.append(0)
1143
+ h.append(sub_h)
1144
+
1145
+ self._h = np.array(h)
1146
+
1147
+
1148
+ def compute_u(self,
1149
+ x: float | np.ndarray,
1150
+ T: float | np.ndarray) -> None:
1151
+ r"""Compute the flow velocity u(x, t) at given time and positions.
1152
+
1153
+ .. math::
1154
+ u(x,t) =
1155
+ \begin{cases}
1156
+ 0 & \text{if } x \leq x_A(t), \\\\
1157
+ \frac{2}{3} \left( \frac{x-x_0}{t} + c_0 + mt \right) & \text{if } x_A(t) < x \leq x_B(t), \\\\
1158
+ 0 & \text{if } x_B(t) < x,
1159
+ \end{cases}
1160
+
1161
+ Parameters
1162
+ ----------
1163
+ x : float or np.ndarray
1164
+ Spatial positions.
1165
+ T : float or np.ndarray
1166
+ Time instant.
1167
+
1168
+ Notes
1169
+ -----
1170
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._u`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
1171
+ """
1172
+ if isinstance(x, float):
1173
+ x = [x]
1174
+ if isinstance(T, float):
1175
+ T = [T]
1176
+
1177
+ self._x = x
1178
+ self._t = T
1179
+
1180
+ # x = [i - max(x) for i in x]
1181
+
1182
+ u = []
1183
+ for t in T:
1184
+ sub_u = []
1185
+ for i in x:
1186
+ if i <= self.xa(t):
1187
+ sub_u.append(np.nan)
1188
+ elif self.xa(t) < i <= self.xb(t):
1189
+ u_val = (2/3) * ( ((i-self._x0)/t) + self._c0 + self._m * t )
1190
+ sub_u.append(u_val)
1191
+ else:
1192
+ sub_u.append(np.nan)
1193
+ u.append(sub_u)
1194
+ self._u = np.array(u)
1195
+
1196
+
1197
+ class Dressler_dry(Depth_result):
1198
+ r"""Dam-break solution on a dry domain with friction using shallow water theory.
1199
+
1200
+ This class implements the 1D analytical Dressler's solution of an ideal dam break on a dry domain with friction.
1201
+ The dam break is instantaneous, over an horizontal and flat surface with friction.
1202
+ It computes the flow height (took verticaly) and velocity over space and time, based on the equation implemanted
1203
+ in SWASHES, based on Dressler's equation.
1204
+
1205
+ Dressler, R.F., 1952, Hydraulic resistance effect upon the dam‑break functions, Journal of Research of the National Bureau
1206
+ of Standards, vol. 49(3), p. 217-225.
1207
+
1208
+ Parameters
1209
+ ----------
1210
+ x_0 : float
1211
+ Initial dam location (position along x-axis).
1212
+ h_0 : float
1213
+ Water depth to the left of the dam.
1214
+ C : float, optional
1215
+ Chézy coefficient, by default 40.
1216
+
1217
+ Attributes
1218
+ ----------
1219
+ _x0 : float
1220
+ Initial dam location (position along x-axis).
1221
+ _h0 : float
1222
+ Water depth to the left of the dam.
1223
+ _c : float, optional
1224
+ Chézy coefficient, by default 40.
1225
+ _xt : float
1226
+ Position of the tip area, by default None.
1227
+ _ht : float, optional
1228
+ Depth of the tip area, by default None.
1229
+ _ut : float, optional
1230
+ Velocity of the tip area, by default None.
1231
+ """
1232
+ def __init__(self,
1233
+ x_0: float,
1234
+ h_0: float,
1235
+ C: float=40
1236
+ ):
1237
+ super().__init__()
1238
+ self._x0 = x_0
1239
+ self._h0 = h_0
1240
+ self._c = C
1241
+ self._xt = []
1242
+
1243
+
1244
+ def xa(self, t: float) -> float:
1245
+ r"""
1246
+ Position of the rarefaction wave front (left-most edge) :
1247
+
1248
+ .. math::
1249
+ x_A(t) = x_0 - t \sqrt{g h_0}
1250
+
1251
+ Parameters
1252
+ ----------
1253
+ t : float
1254
+ Time instant.
1255
+
1256
+ Returns
1257
+ -------
1258
+ float
1259
+ Position of the front edge of the rarefaction wave.
1260
+ """
1261
+ return self._x0 - (t * np.sqrt(self._g*self._h0))
1262
+
1263
+
1264
+ def xb(self, t: float) -> float:
1265
+ r"""
1266
+ Position of the contact discontinuity:
1267
+
1268
+ .. math::
1269
+ x_B(t) = x_0 + 2 t \sqrt{g h_0}
1270
+
1271
+ Parameters
1272
+ ----------
1273
+ t : float
1274
+ Time instant.
1275
+
1276
+ Returns
1277
+ -------
1278
+ float
1279
+ Position of the contact wave (end of rarefaction).
1280
+ """
1281
+ return self._x0 + (2 * t * np.sqrt(self._g*self._h0))
1282
+
1283
+
1284
+ def alpha1(self, x: float, t: float) -> float:
1285
+ r"""
1286
+ Correction coefficient for the height:
1287
+
1288
+ .. math::
1289
+ \alpha_1(\xi) = \frac{6}{5(2-\xi)} - \frac{2}{3} + \frac{4 \sqrt{3}}{135} (2-\xi)^{3/2}), \\\\
1290
+
1291
+ with :math:`\xi = \frac{x-x_0}{t\sqrt{g h_0}}`
1292
+
1293
+ Parameters
1294
+ ----------
1295
+ x : float
1296
+ Spatial position.
1297
+ t : float
1298
+ Time instant.
1299
+
1300
+ Returns
1301
+ -------
1302
+ float
1303
+ Correction coefficient.
1304
+ """
1305
+ xi = (x-self._x0)/(t*np.sqrt(self._g*self._h0))
1306
+ # return (6 / (5*(2 - (x/(t*np.sqrt(self._g*self._h0)))))) - (2/3) + (4*np.sqrt(3)/135)*((2 - (x/(t*np.sqrt(self._g*self._h0))))**(3/2))
1307
+ # return (6 / (5 * (2 - xi))) - (2 / 3) + (4 * np.sqrt(3) / 135) * (2 - xi) ** (3 / 2)
1308
+ if xi < 2:
1309
+ return (6 / (5 * (2 - xi))) - (2 / 3) + (4 * np.sqrt(3) / 135) * (2 - xi) ** (3 / 2)
1310
+ else:
1311
+ return 0
1312
+
1313
+
1314
+ def alpha2(self, x: float, t: float) -> float:
1315
+ r"""
1316
+ Correction coefficient for the velocity:
1317
+
1318
+ .. math::
1319
+ \alpha_2(\xi) = \frac{12}{2-(2-\xi)} - \frac{8}{3} + \frac{8 \sqrt{3}}{189} (2-\xi)^{3/2}) - \frac{108}{7(2 - \xi)}, \\\\
1320
+
1321
+ with :math:`\xi = \frac{x-x_0}{t\sqrt{g h_0}}`
1322
+
1323
+ Parameters
1324
+ ----------
1325
+ x : float
1326
+ Spatial position.
1327
+ t : float
1328
+ Time instant.
1329
+
1330
+ Returns
1331
+ -------
1332
+ float
1333
+ Correction coefficient.
1334
+ """
1335
+ xi = (x-self._x0)/(t*np.sqrt(self._g*self._h0))
1336
+
1337
+ if xi < 2:
1338
+ return 12./(2 - xi)- 8/3 + 8*np.sqrt(3)/189 * ((2 - xi)**(3/2)) - 108/(7*(2 - xi)**2)
1339
+ else:
1340
+ return 0
1341
+
1342
+
1343
+ def compute_u(self,
1344
+ x: float | np.ndarray,
1345
+ T: float | np.ndarray
1346
+ ) -> None:
1347
+ """Call :meth:`compute_h`."""
1348
+ if self._u is None:
1349
+ self.compute_h(x, T)
1350
+
1351
+
1352
+ def compute_h(self,
1353
+ x: float | np.ndarray,
1354
+ T: float | np.ndarray
1355
+ ) -> None:
1356
+ r"""Compute the flow height h(x, t) and velocity u(x, t) at given time and positions.
1357
+
1358
+ .. math::
1359
+ h(x, t) =
1360
+ \begin{cases}
1361
+ h_0 & \text{if } x \leq x_A(t), \\\\
1362
+ \frac{1}{g} \left( \frac{2}{3} \sqrt{g h_0} - \frac{x - x_0}{3t} + \frac{g^{2}}{C^2} \alpha_1 t \right)^2 & \text{if } x_A(t) < x \leq x_t(t), \\\\
1363
+ \frac{-b-\sqrt{b^2 - 4 a (c-x(t))}}{2 a} & \text{if } x_t(t) < x \leq x_B(t), \\\\
1364
+ 0 & \text{if } x_B(t) < x,
1365
+ \end{cases}
1366
+
1367
+ with :math:`r = \left. \frac{dx}{dh} \right|_{h = h_t}`, :math:`c = x_B(t)`, :math:`a = \frac{r h_t + c - x_t}{h_t^2}`, :math:`b = r - 2 a h_t`. :math:`x_t` and :math:`h_t` being the position
1368
+ and the flow depth at the beginning of the tip area.
1369
+
1370
+ .. math::
1371
+ u(x,t) =
1372
+ \begin{cases}
1373
+ 0 & \text{if } x \leq x_A(t), \\\\
1374
+ u_{co} = \frac{2\sqrt{g h_0}}{3} + \frac{2(x - x_0)}{3t} + \frac{g^2}{C^2} \alpha_2 t & \text{if } x_A(t) < x \leq x_t(t), \\\\
1375
+ \max_{x \in [x_A(t), x_t(t)]} u_{co}(x, t) & \text{if } x_t(t) < x \leq x_B(t), \\\\
1376
+ 0 & \text{if } x_B(t) < x,
1377
+ \end{cases}
1378
+
1379
+ Parameters
1380
+ ----------
1381
+ x : float | np.ndarray
1382
+ Spatial positions.
1383
+ T : float | np.ndarray
1384
+ Time instant.
1385
+
1386
+ Notes
1387
+ -----
1388
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._h`, :attr:`tilupy.analytic_sol.Depth_result._u`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
1389
+ """
1390
+ if isinstance(x, float):
1391
+ x = [x]
1392
+ if isinstance(T, float):
1393
+ T = [T]
1394
+
1395
+ self._x = x
1396
+ self._t = T
1397
+
1398
+ h = []
1399
+ u = []
1400
+
1401
+ xt = None
1402
+ ht = None
1403
+ ut = None
1404
+
1405
+ for t in T:
1406
+ sub_h = []
1407
+ sub_u = []
1408
+ for i in x:
1409
+ if i <= self.xa(t):
1410
+ sub_h.append(self._h0)
1411
+ sub_u.append(np.nan)
1412
+
1413
+ elif self.xa(t) < i <= self.xb(t):
1414
+ if t == 0:
1415
+ t = 1e-18
1416
+ term = ((2/3)*np.sqrt(self._g*self._h0)) - ((i - self._x0) / (3*t)) + ((self._g**2)/(self._c**2)) * self.alpha1(i, t) * t
1417
+ h_val = (1/self._g) * term**2
1418
+
1419
+ if sub_u[-1] is np.nan:
1420
+ sub_u[-1] = 0
1421
+ u_val = (2/3)*np.sqrt(self._g*self._h0)*(1+(i-self._x0)/(np.sqrt(self._g*self._h0)*t)) + ((self._g**2)/(self._c**2))*self.alpha2(i, t)*t
1422
+
1423
+ if u_val < sub_u[-1] and xt is None:
1424
+ xt = i
1425
+ ht = sub_h[-1]
1426
+ ut = sub_u[-1]
1427
+
1428
+ dx = x[1] - x[0]
1429
+ dh = sub_h[-1] - sub_h[-2]
1430
+
1431
+ r = dx / dh
1432
+ c = self.xb(t)
1433
+ a = (r * ht + c - xt) / (ht ** 2)
1434
+ b = r - 2 * a * ht
1435
+
1436
+ if xt is not None:
1437
+ u_val = ut
1438
+ h_val = (-b-np.sqrt((b**2) - 4.*a*(c-i))) / (2*a)
1439
+
1440
+ sub_h.append(h_val)
1441
+ sub_u.append(u_val)
1442
+
1443
+ else:
1444
+ sub_h.append(0)
1445
+ sub_u.append(np.nan)
1446
+ h.append(sub_h)
1447
+ u.append(sub_u)
1448
+
1449
+ self._xt.append(xt)
1450
+ xt = None
1451
+ ht = None
1452
+ ut = None
1453
+
1454
+ self._h = np.array(h)
1455
+ self._u = np.array(u)
1456
+
1457
+
1458
+ class Chanson_dry(Depth_result):
1459
+ r"""Dam-break solution on a dry domain with friction using shallow water theory.
1460
+
1461
+ This class implements the 1D analytical Chanson's solution of an ideal dam break on a dry domain with friction.
1462
+ The dam break is instantaneous, over an horizontal and flat surface with friction.
1463
+ It computes the flow height (took verticaly) and velocity over space and time, based on the equation implemanted
1464
+ in SWASHES, based on Chanson's equation.
1465
+
1466
+ Chanson, H., 2005, Applications of the Saint-Venant Equations and Method of Characteristics to the Dam Break Wave Problem. https://espace.library.uq.edu.au/view/UQ:9438
1467
+
1468
+ Parameters
1469
+ ----------
1470
+ x_0 : float
1471
+ Initial dam location (position along x-axis).
1472
+ h_0 : float
1473
+ Water depth to the left of the dam.
1474
+ f : float
1475
+ Darcy friction factor.
1476
+
1477
+ Attributes
1478
+ ----------
1479
+ _x0 : float
1480
+ Initial dam location (position along x-axis).
1481
+ _h0 : float
1482
+ Water depth to the left of the dam.
1483
+ _f : float, optional
1484
+ Darcy friction factor.
1485
+ """
1486
+ def __init__(self,
1487
+ h_0: float,
1488
+ x_0: float,
1489
+ f: float
1490
+ ):
1491
+ super().__init__()
1492
+ self._h0 = h_0
1493
+ self._x0 = x_0
1494
+ self._f = f
1495
+
1496
+
1497
+ def xa(self, t: float) -> float:
1498
+ r"""
1499
+ Position of the rarefaction wave front (left-most edge) :
1500
+
1501
+ .. math::
1502
+ x_A(t) = x_0 - t \sqrt{g h_0}
1503
+
1504
+ Parameters
1505
+ ----------
1506
+ t : float
1507
+ Time instant.
1508
+
1509
+ Returns
1510
+ -------
1511
+ float
1512
+ Position of the front edge of the rarefaction wave.
1513
+ """
1514
+ return self._x0 - (t * np.sqrt(self._g*self._h0))
1515
+
1516
+
1517
+ def xb(self, t: float) -> float:
1518
+ r"""
1519
+ Position of the tip of the flow:
1520
+
1521
+ .. math::
1522
+ x_B(t) = x_0 + \left( \frac{3}{2} \frac{U(t)}{\sqrt{g h_0}} - 1 \right) t \sqrt{g h_0}
1523
+
1524
+ Parameters
1525
+ ----------
1526
+ t : float
1527
+ Time instant.
1528
+
1529
+ Returns
1530
+ -------
1531
+ float
1532
+ Position of the flow tip.
1533
+ """
1534
+ cf = self.compute_cf(t)
1535
+ return self._x0 + ((3*cf)/(2*np.sqrt(self._g*self._h0))-1) * (t*np.sqrt(self._g*self._h0))
1536
+ # return ((3/2) * cf - np.sqrt(self._g * self._h0)) * t
1537
+
1538
+
1539
+ def xc(self, t: float) -> float:
1540
+ r"""
1541
+ Position of the contact discontinuity:
1542
+
1543
+ .. math::
1544
+ x_C(t) = x_0 + \left( \frac{3}{2} \frac{U(t)}{\sqrt{g h_0}} - 1 \right) t \sqrt{\frac{g}{h_0}} + \frac{4}{f\frac{U(t)^2}{g h_0}} \left( 1 - \frac{U(t)}{2 \sqrt{g h_0}} \right)^4
1545
+
1546
+ Parameters
1547
+ ----------
1548
+ t : float
1549
+ Time instant.
1550
+
1551
+ Returns
1552
+ -------
1553
+ float
1554
+ Position of the contact wave (wave front).
1555
+ """
1556
+ cf = self.compute_cf(t)
1557
+
1558
+ term1 = ((1.5 * (cf / np.sqrt(self._g * self._h0))) - 1) * np.sqrt(self._g / self._h0) * t
1559
+ term2 = (4 / (self._f * ((cf**2) / (self._g * self._h0)))) * (1 - 0.5 * (cf / np.sqrt(self._g * self._h0)))**4
1560
+
1561
+ x_s = self._x0 + self._h0 * (term1 + term2)
1562
+ return x_s
1563
+
1564
+
1565
+ def compute_cf(self, t: float) -> float:
1566
+ r"""Compute the celerity of the wave front by resolving:
1567
+
1568
+ .. math::
1569
+ \left( \frac{U}{\sqrt{g h_0}} \right)^3 - 8 \left( 0.75 - \frac{3 f t \sqrt{g}}{8 \sqrt{h_0}} \right) \left( \frac{U}{\sqrt{g h_0}} \right)^2 + 12 \left( \frac{U}{\sqrt{g h_0}} \right) - 8 = 0
1570
+
1571
+ Parameters
1572
+ ----------
1573
+ t : float
1574
+ Time instant
1575
+
1576
+ Returns
1577
+ -------
1578
+ float
1579
+ Value of the front wave velocity.
1580
+ """
1581
+ coeffs = [1, (-8*(0.75 - ((3 * self._f * t * np.sqrt(self._g)) / (8 * np.sqrt(self._h0))))), 12, -8]
1582
+ roots = np.roots(coeffs)
1583
+
1584
+ real_root = roots[-1].real
1585
+ return real_root * np.sqrt(self._g * self._h0)
1586
+
1587
+
1588
+ def compute_h(self,
1589
+ x: float | np.ndarray,
1590
+ T: float | np.ndarray
1591
+ ) -> None:
1592
+ r"""Compute the flow height h(x, t) at given time and positions.
1593
+
1594
+ .. math::
1595
+ h(x, t) =
1596
+ \begin{cases}
1597
+ h_0 & \text{if } x \leq x_A(t), \\\\
1598
+ \frac{4}{9g} \left( \sqrt{g h_0} - \frac{x - x_0}{2t} \right)^2 & \text{if } x_A(t) < x \leq x_B(t), \\\\
1599
+ \sqrt{\frac{f}{4} \frac{U(t)^2}{g h_0} \frac{x_C(t)-x}{h_0}} & \text{if } x_B(t) < x \leq x_C(t), \\\\
1600
+ 0 & \text{if } x_C(t) < x
1601
+ \end{cases}
1602
+
1603
+ Parameters
1604
+ ----------
1605
+ x : float or np.ndarray
1606
+ Spatial positions.
1607
+ T : float or np.ndarray
1608
+ Time instant.
1609
+
1610
+ Notes
1611
+ -----
1612
+ Updates the internal :attr:`tilupy.analytic_sol.Depth_result._h`, :attr:`tilupy.analytic_sol.Depth_result._x`, :attr:`tilupy.analytic_sol.Depth_result._t` attributes with the computed result.
1613
+ """
1614
+ if isinstance(x, float):
1615
+ x = [x]
1616
+ if isinstance(T, float):
1617
+ T = [T]
1618
+
1619
+ self._x = x
1620
+ self._t = T
1621
+
1622
+ h = []
1623
+ for t in T:
1624
+ sub_h = []
1625
+ cf = self.compute_cf(t)
1626
+
1627
+ for i in x:
1628
+ if i <= self.xa(t):
1629
+ sub_h.append(self._h0)
1630
+
1631
+ elif self.xa(t) < i <= self.xb(t):
1632
+ sub_h.append((4/(9*self._g)) * (np.sqrt(self._g*self._h0)-((i-self._x0)/(2*t)))**2)
1633
+
1634
+ elif self.xb(t) <= i <= self.xc(t):
1635
+ # h_left = (4/(9*self._g)) * (np.sqrt(self._g*self._h0) - (self.xb(t)/(2*t)))**2
1636
+ # K = (h_left / self._h0)**2 / ((self.xc(t) - self.xb(t)) / self._h0)
1637
+ # term = K * ((self.xc(t) - i) / self._h0)
1638
+ # val = np.sqrt(term) * self._h0
1639
+
1640
+ # h_left = (4/(9*self._g)) * (np.sqrt(self._g*self._h0) - (self.xb(t)/(2*t)))**2
1641
+ # term_denominator = (self._f / 4) * ((cf**2) / (self._g * self._h0)) * ((self.xc(t)-self.xb(t)) / self._h0)
1642
+ # val_term_at_xb = np.sqrt(term_denominator) * self._h0
1643
+ # C = h_left / val_term_at_xb
1644
+ # term = (self._f / 4) * ((cf**2) / (self._g * self._h0)) * ((self.xc(t)-i) / self._h0)
1645
+ # val = C * np.sqrt(term) * self._h0
1646
+
1647
+ term = (self._f / 4) * ((cf**2) / (self._g * self._h0)) * ((self.xc(t)-(i)) / self._h0)
1648
+ val = np.sqrt(term) * self._h0
1649
+ sub_h.append(val)
1650
+
1651
+ else:
1652
+ sub_h.append(0)
1653
+ h.append(sub_h)
1654
+ self._h = np.array(h)
1655
+
1656
+
1657
+ def compute_u(self,
1658
+ x: float | np.ndarray,
1659
+ T: float | np.ndarray
1660
+ ) -> None:
1661
+ r"""No solution"""
1662
+ self._u = None
1663
+
1664
+
1665
+ class Shape_result(ABC):
1666
+ """Abstract base class representing shape results of a simulated flow.
1667
+
1668
+ This class defines a common interface for flow simulation that compute
1669
+ the geometry of the final shape of a flow simulation.
1670
+
1671
+ Parameters
1672
+ ----------
1673
+ theta : float, optional
1674
+ Angle of the surface, in radian, by default 0.
1675
+
1676
+ Attributes
1677
+ ----------
1678
+ _g = 9.81 : float
1679
+ Gravitational constant.
1680
+ _theta : float
1681
+ Angle of the surface, in radian.
1682
+ _x : float or np.ndarray
1683
+ Spatial coordinates.
1684
+ _h : np.ndarray
1685
+ Flow height depending on space.
1686
+ """
1687
+ def __init__(self,
1688
+ theta: float=0):
1689
+ self._g = 9.81
1690
+ self._theta = theta
1691
+
1692
+ self._x = None
1693
+ self._y = None
1694
+ self._h = None
1695
+
1696
+
1697
+ @property
1698
+ def h(self):
1699
+ """Accessor of the shape h of the flow.
1700
+
1701
+ Returns
1702
+ -------
1703
+ numpy.ndarray
1704
+ Attribute :attr:`_h`. If None, no solution computed.
1705
+ """
1706
+ return self._h
1707
+
1708
+
1709
+ @property
1710
+ def x(self):
1711
+ """Accessor of the spatial distribution of the computed solution.
1712
+
1713
+ Returns
1714
+ -------
1715
+ numpy.ndarray
1716
+ Attribute :attr:`_x`. If None, no solution computed.
1717
+ """
1718
+ return self._x
1719
+
1720
+
1721
+ @property
1722
+ def y(self):
1723
+ """Accessor of the lateral spatial distribution of the computed solution.
1724
+
1725
+ Returns
1726
+ -------
1727
+ numpy.ndarray
1728
+ Attribute :attr:`_y`. If None, no solution computed.
1729
+ """
1730
+ return self._y
1731
+
1732
+
1733
+ class Coussot_shape(Shape_result):
1734
+ r"""Shape solution on an inclined dry domain without friction.
1735
+
1736
+ This class implements the final shape of a simulated flow.
1737
+ The flow is over an inclined and flat surface without friction with a finite volume of fluid.
1738
+ It computes the spatial coordinates from the flow lenght and height.
1739
+
1740
+ Coussot, P., Proust, S., & Ancey, C., 1996, Rheological interpretation of deposits of yield stress fluids,
1741
+ Journal of Non-Newtonian Fluid Mechanics, v. 66(1), p. 55-70, doi:10.1016/0377-0257(96)01474-7.
1742
+
1743
+ Parameters
1744
+ ----------
1745
+ rho : float
1746
+ Fluid density.
1747
+ tau : float
1748
+ Threshold constraint.
1749
+ theta : float, optional
1750
+ Angle of the surface, in degree, by default 0.
1751
+ h_final : float, optional
1752
+ The final flow depth, by default 1.
1753
+ H_size : int, optional
1754
+ Number of value wanted in the H array, by default 100.
1755
+
1756
+ Attributes
1757
+ ----------
1758
+ _rho : float
1759
+ Fluid density.
1760
+ _tau : float
1761
+ Threshold constraint.
1762
+ _D : float or numpy.ndarray
1763
+ Normalized distance of the front from the origin.
1764
+ _H : float or numpy.ndarray
1765
+ Normalized fluid depth.
1766
+ _d : float or numpy.ndarray
1767
+ Distance of the front from the origin.
1768
+ _h : float or numpy.ndarray
1769
+ Fluid depth.
1770
+ _H_size : int
1771
+ Number of point in H-axis.
1772
+
1773
+ """
1774
+ def __init__(self,
1775
+ rho: float,
1776
+ tau: float,
1777
+ theta: float=0,
1778
+ h_final: float=1,
1779
+ H_size: int=100
1780
+ ):
1781
+ super().__init__(np.radians(theta))
1782
+ self._rho = rho
1783
+ self._tau = tau
1784
+
1785
+ self._H_size = H_size
1786
+ self._D = None
1787
+ self._d = None
1788
+ if theta>0 and self.h_to_H(h_final) >=1:
1789
+ self._H = np.linspace(0, 0.99999999, H_size)
1790
+ else:
1791
+ self._H = np.linspace(0, self.h_to_H(h_final), H_size)
1792
+ self._h = np.array([self.H_to_h(H) for H in self._H])
1793
+
1794
+
1795
+ def h_to_H(self,
1796
+ h: float
1797
+ ) -> float:
1798
+ r"""Normalize the fluid depth by following:
1799
+
1800
+ .. math::
1801
+ H = \frac{\rho g h \sin(\theta)}{\tau_c}
1802
+
1803
+ If :math:`\theta = 0`, the expression is:
1804
+
1805
+ .. math::
1806
+ H = \frac{\rho g h}{\tau_c}
1807
+
1808
+ Parameters
1809
+ ----------
1810
+ h : float
1811
+ Initial fluid depth.
1812
+
1813
+ Returns
1814
+ -------
1815
+ float
1816
+ Normalized fluid depth.
1817
+ """
1818
+ if self._theta == 0:
1819
+ return (self._rho*self._g*h)/self._tau
1820
+ else:
1821
+ return (self._rho*self._g*h*np.sin(self._theta))/self._tau
1822
+
1823
+
1824
+ def H_to_h(self,
1825
+ H: float
1826
+ ) -> float:
1827
+ r"""Find the original value of the fluid depth from the normalized one
1828
+ by following:
1829
+
1830
+ .. math::
1831
+ h = \frac{H \tau_c}{\rho g \sin(\theta)}
1832
+
1833
+ If :math:`\theta = 0`, the expression is:
1834
+
1835
+ .. math::
1836
+ h = \frac{H \tau_c}{\rho g}
1837
+
1838
+ Parameters
1839
+ ----------
1840
+ H : float
1841
+ Normalized value of the fluid depth.
1842
+
1843
+ Returns
1844
+ -------
1845
+ float
1846
+ True value of the fluid depth.
1847
+ """
1848
+ if self._theta == 0:
1849
+ return ((H*self._tau)/(self._rho*self._g))
1850
+ else:
1851
+ return ((H*self._tau)/(self._rho*self._g*np.sin(self._theta)))
1852
+
1853
+
1854
+ def x_to_X(self,
1855
+ x: float
1856
+ ) -> float:
1857
+ r"""Normalize the spatial coordinates by following:
1858
+
1859
+ .. math::
1860
+ X = \frac{\rho g x (\sin(\theta))^2}{\tau_c \cos(\theta)}
1861
+
1862
+ If :math:`\theta = 0`, the expression is:
1863
+
1864
+ .. math::
1865
+ X = \frac{\rho g x}{\tau_c}
1866
+
1867
+ Parameters
1868
+ ----------
1869
+ x : float
1870
+ Initial spatial coordinate.
1871
+
1872
+ Returns
1873
+ -------
1874
+ float
1875
+ Normalized spatial coordinate.
1876
+ """
1877
+ if self._theta == 0:
1878
+ return (self._rho*self._g*x)/self._tau
1879
+ else:
1880
+ return (self._rho*self._g*x*np.sin(self._theta)*np.sin(self._theta)) / (self._tau*np.cos(self._theta))
1881
+
1882
+
1883
+ def X_to_x(self,
1884
+ X: float
1885
+ ) -> float:
1886
+ r"""Find the original value of the spatial coordinates from the normalized one
1887
+ by following:
1888
+
1889
+ .. math::
1890
+ x = \frac{X \tau_c \cos(\theta)}{\rho g (\sin(\theta))^2}
1891
+
1892
+ If :math:`\theta = 0`, the expression is:
1893
+
1894
+ .. math::
1895
+ x = \frac{X \tau_c}{\rho g}
1896
+
1897
+ Parameters
1898
+ ----------
1899
+ X : float
1900
+ Normalized values of the spatial coordinates.
1901
+
1902
+ Returns
1903
+ -------
1904
+ float
1905
+ True value of the spatial coordinate.
1906
+ """
1907
+ if self._theta == 0:
1908
+ return (X*self._tau)/(self._rho*self._g)
1909
+ else:
1910
+ return (X*self._tau*np.cos(self._theta))/(self._rho*self._g*np.sin(self._theta)*np.sin(self._theta))
1911
+
1912
+
1913
+ def compute_rheological_test_front_morpho(self) -> None:
1914
+ r"""Compute the shape of the frontal lobe from the normalized fluid depth for a rheological test on an inclined
1915
+ surface by following :
1916
+
1917
+ .. math::
1918
+ D = - H - \ln(1 - H)
1919
+
1920
+ If :math:`\theta = 0`, the expression is:
1921
+
1922
+ .. math::
1923
+ D = \frac{H^2}{2}
1924
+ """
1925
+ if self._theta == 0:
1926
+ D = []
1927
+ d = []
1928
+ for H_val in self._H:
1929
+ D.append((H_val*H_val)/2)
1930
+ d.append(self.X_to_x(D[-1]))
1931
+
1932
+ else:
1933
+ D = []
1934
+ d = []
1935
+ for H_val in self._H:
1936
+ D.append(- H_val - np.log(1 - H_val))
1937
+ d.append(self.X_to_x(D[-1]))
1938
+
1939
+ self._D = np.array(D)
1940
+ self._d = np.array(d)
1941
+
1942
+
1943
+ def compute_rheological_test_lateral_morpho(self) -> None:
1944
+ r"""Compute the shape of the lateral lobe from the normalized fluid depth for a rheological test on an inclined
1945
+ surface by following :
1946
+
1947
+ .. math::
1948
+ D = 1 - \sqrt{1 - H^2}
1949
+ """
1950
+ D = []
1951
+ d = []
1952
+ for H_val in self._H:
1953
+ D.append(1 - np.sqrt(1 - (H_val**2)))
1954
+ d.append(self.X_to_x(D[-1]))
1955
+
1956
+ self._D = np.array(D)
1957
+ self._d = np.array(d)
1958
+
1959
+
1960
+ def compute_slump_test_hf(self, h_init: float) -> float:
1961
+ r"""Compute the final fluid depth for a cylindrical slump test following :
1962
+
1963
+ .. math::
1964
+ \frac{h_f}{h_i} = 1 - \frac{2 \tau_c}{\rho g h_i} \left( 1 - \ln{\frac{2 \tau_c}{\rho g h_i}} \right)
1965
+
1966
+ N. Pashias, D. V. Boger, J. Summers, D. J. Glenister; A fifty cent rheometer for yield stress measurement. J. Rheol.
1967
+ 1 November 1996; 40 (6): 1179-1189. https://doi.org/10.1122/1.550780
1968
+
1969
+ Parameters
1970
+ ----------
1971
+ h_init : float
1972
+ Initial fluid depth.
1973
+
1974
+ Returns
1975
+ -------
1976
+ float
1977
+ Final fluid depth
1978
+ """
1979
+ H_init = self.h_to_H(h_init)
1980
+ val = 1 - ((2/H_init)*(1-np.log(2/H_init)))
1981
+ return self.H_to_h(val*H_init)
1982
+
1983
+
1984
+ def translate_front(self, d_final: float) -> None:
1985
+ """Translate the shape of the frontal (or transversal) lobe to the wanted x (or y) coordinate.
1986
+
1987
+ Parameters
1988
+ ----------
1989
+ d_final : float
1990
+ Final wanted coordinate.
1991
+ """
1992
+ self._d += d_final
1993
+
1994
+
1995
+ def change_orientation_flow(self) -> None:
1996
+ """Swap the direction of the result.
1997
+
1998
+ Notes
1999
+ ------
2000
+ There must not have been any prior translation to use this method.
2001
+ """
2002
+ self._h = self._h[::-1]
2003
+ new_d = [-1*v for v in self._d]
2004
+ self._d = np.array(new_d[::-1])
2005
+
2006
+
2007
+ def interpolate_on_d(self) -> None:
2008
+ """Interpolate the profile on d-axis.
2009
+ """
2010
+ from scipy.interpolate import interp1d
2011
+
2012
+ d_min, d_max = self._d.min(), self._d.max()
2013
+ d_curve = np.linspace(d_min, d_max, self._H_size)
2014
+
2015
+ f = interp1d(self._d, self._h, kind='cubic')
2016
+ h_curve = f(d_curve)
2017
+
2018
+ self._d = d_curve
2019
+ self._h = h_curve
2020
+
2021
+
2022
+ @property
2023
+ def d(self):
2024
+ """Accessor of the spatial distribution of the computed solution.
2025
+
2026
+ Returns
2027
+ -------
2028
+ numpy.ndarray
2029
+ Attribute :attr:`_d`. If None, no solution computed.
2030
+ """
2031
+ return self._d
2032
+
2033
+
2034
+ class Front_result:
2035
+ """Class computing front position of a simulated flow.
2036
+
2037
+ This class defines multiple methods for flow simulation that compute
2038
+ the position of the front flow at the specified moment.
2039
+
2040
+ Parameters
2041
+ ----------
2042
+ h0 : float
2043
+ Initial fluid depth.
2044
+
2045
+ Attributes
2046
+ ----------
2047
+ _g = 9.81 : float
2048
+ Gravitational constant.
2049
+ _h0 : float
2050
+ Initial fluid depth.
2051
+ _xf : dictionnary
2052
+ Dictionnary of spatial coordinates of the front flow for each time step (keys).
2053
+ _labels : dictionnary
2054
+ Dictionnary of spatial coordinates computation's method for each time step (keys).
2055
+ """
2056
+ def __init__(self,
2057
+ h0: float,
2058
+ ):
2059
+ self._g = 9.81
2060
+
2061
+ self._h0 = h0
2062
+
2063
+ self._xf = {}
2064
+ self._labels = {}
2065
+
2066
+
2067
+ def xf_mangeney(self,
2068
+ t: float,
2069
+ delta: float,
2070
+ theta: float=0
2071
+ ) -> float:
2072
+ r"""
2073
+ Mangeney's equation for a dam-break solution over an infinite inclined dry domain with friction
2074
+ and an infinitely-long fluid mass:
2075
+
2076
+ .. math::
2077
+ x_f(t) = \frac{1}{2}mt^2 + 2 c_0 t
2078
+
2079
+ with :math:`c_0` the initial wave propagation speed defined by:
2080
+
2081
+ .. math::
2082
+ c_0 = \sqrt{g h_0 \cos{\theta}}
2083
+
2084
+ and :math:`m` the constant horizontal acceleration of the front defined by:
2085
+
2086
+ .. math::
2087
+ m = g \sin{\theta} - g \cos{\theta} \tan{\delta}
2088
+
2089
+ Mangeney, A., Heinrich, P., & Roche, R., 2000, Analytical solution for testing debris avalanche numerical models,
2090
+ Pure and Applied Geophysics, vol. 157, p. 1081-1096.
2091
+
2092
+ Parameters
2093
+ ----------
2094
+ t : float
2095
+ Time instant.
2096
+ delta : float
2097
+ Dynamic friction angle, in degree.
2098
+ theta : float
2099
+ Slope angle, in degree.
2100
+
2101
+ Returns
2102
+ -------
2103
+ float
2104
+ Position of the front edge of the fluid.
2105
+ """
2106
+ theta_rad = np.radians(theta)
2107
+ delta_rad = np.radians(delta)
2108
+
2109
+ m = self._g * np.sin(theta_rad) - (self._g * np.cos(theta_rad) * np.tan(delta_rad))
2110
+ c0 = np.sqrt(self._g * self._h0 * np.cos(theta_rad))
2111
+ xf = 0.5*m*(t**2) + (2*c0*t)
2112
+
2113
+ if t in self._labels:
2114
+ if f"Mangeney d{delta}" not in self._labels[t] :
2115
+ self._labels[t].append(f"Mangeney d{delta}")
2116
+ self._xf[t].append(xf)
2117
+ else:
2118
+ self._labels[t] = [f"Mangeney d{delta}"]
2119
+ self._xf[t] = [xf]
2120
+
2121
+ return xf
2122
+
2123
+
2124
+ def xf_dressler(self,
2125
+ t: float,
2126
+ ) -> float:
2127
+ r"""
2128
+ Dressler's equation for a dam-break solution over an infinite inclined dry domain with friction:
2129
+
2130
+ .. math::
2131
+ x_f(t) = 2 t \sqrt{g h_0}
2132
+
2133
+ Parameters
2134
+ ----------
2135
+ t : float
2136
+ Time instant.
2137
+
2138
+ Returns
2139
+ -------
2140
+ float
2141
+ Position of the front edge of the fluid.
2142
+ """
2143
+ xf = 2 * t * np.sqrt(self._g*self._h0)
2144
+
2145
+ if t in self._labels:
2146
+ if "Dressler" not in self._labels[t] :
2147
+ self._labels[t].append("Dressler")
2148
+ self._xf[t].append(xf)
2149
+ else:
2150
+ self._labels[t] = ["Dressler"]
2151
+ self._xf[t] = [xf]
2152
+
2153
+ return xf
2154
+
2155
+
2156
+ def xf_ritter(self,
2157
+ t: float
2158
+ ) -> float:
2159
+ r"""
2160
+ Ritter's equation for a dam-break solution over an infinite inclined dry domain without friction:
2161
+
2162
+ .. math::
2163
+ x_f(t) = 2 t \sqrt{g h_0}
2164
+
2165
+ Ritter A. Die Fortpflanzung der Wasserwellen. Zeitschrift des Vereines Deuscher Ingenieure
2166
+ August 1892; 36(33): 947-954.
2167
+
2168
+ Parameters
2169
+ ----------
2170
+ t : float
2171
+ Time instant.
2172
+
2173
+ Returns
2174
+ -------
2175
+ float
2176
+ Position of the front edge of the fluid.
2177
+ """
2178
+ xf = 2 * t * np.sqrt(self._g*self._h0)
2179
+
2180
+ if t in self._labels:
2181
+ if "Ritter" not in self._labels[t] :
2182
+ self._labels[t].append("Ritter")
2183
+ self._xf[t].append(xf)
2184
+ else:
2185
+ self._labels[t] = ["Ritter"]
2186
+ self._xf[t] = [xf]
2187
+
2188
+ return xf
2189
+
2190
+
2191
+ def xf_stoker(self,
2192
+ t: float,
2193
+ hr: float
2194
+ ) -> float:
2195
+ r"""
2196
+ Stoker's equation for a dam-break solution over an infinite inclined wet domain without friction:
2197
+
2198
+ .. math::
2199
+ x_f(t) =t c_m
2200
+
2201
+ with :math:`c_m` the front wave velocity solution of:
2202
+
2203
+ .. math::
2204
+ c_m h_r - h_r \left( \sqrt{1 + \frac{8 c_m^2}{g h_r}} - 1 \right) \left( \frac{c_m}{2} - \sqrt{g h_0} + \sqrt{\frac{g h_r}{2} \left( \sqrt{1 + \frac{8 c_m^2}{g h_r}} - 1 \right)} \right) = 0
2205
+
2206
+ Stoker JJ. Water Waves: The Mathematical Theory with Applications, Pure and Applied Mathematics,
2207
+ Vol. 4. Interscience Publishers: New York, USA, 1957.
2208
+
2209
+ Sarkhosh, P. (2021). Stoker solution package (1.0.0). Zenodo. https://doi.org/10.5281/zenodo.5598374
2210
+
2211
+ Parameters
2212
+ ----------
2213
+ t : float
2214
+ Time instant.
2215
+ hr : float
2216
+ Fluid depth at the right of the dam.
2217
+
2218
+ Returns
2219
+ -------
2220
+ float
2221
+ Position of the front edge of the fluid.
2222
+ """
2223
+ f_cm = 1
2224
+ df_cm = 1
2225
+ cm = 10 * self._h0
2226
+
2227
+ while abs(f_cm / cm) > 1e-10:
2228
+ root_term = np.sqrt(8 * cm**2 / np.sqrt(self._g * hr)**2 + 1)
2229
+ inner_sqrt = np.sqrt(self._g * hr * (root_term - 1) / 2)
2230
+
2231
+ f_cm = cm * hr - hr * (root_term - 1) * (cm / 2 - np.sqrt(self._g * self._h0) + inner_sqrt)
2232
+ df_cm = (hr
2233
+ - hr * ((2 * cm * self._g * hr) / (np.sqrt(self._g * hr)**2 * root_term * inner_sqrt) + 0.5) * (root_term - 1)
2234
+ - (8 * cm * hr * (cm / 2 - np.sqrt(self._g * self._h0) + inner_sqrt)) / (np.sqrt(self._g * hr)**2 * root_term))
2235
+
2236
+ cm -= f_cm / df_cm
2237
+
2238
+ xf = cm * t
2239
+
2240
+ if t in self._labels:
2241
+ if "Stoker" not in self._labels[t] :
2242
+ self._labels[t].append("Stoker")
2243
+ self._xf[t].append(xf)
2244
+ else:
2245
+ self._labels[t] = ["Stoker"]
2246
+ self._xf[t] = [xf]
2247
+
2248
+ return xf
2249
+
2250
+
2251
+ def xf_chanson(self,
2252
+ t: float,
2253
+ f: float
2254
+ ) -> float:
2255
+ r"""
2256
+ Chanson's equation for a dam-break solution over an infinite inclined dry domain with friction:
2257
+
2258
+ .. math::
2259
+ x_f(t) = \left( \frac{3}{2} \frac{U(t)}{\sqrt{g h_0}} - 1 \right) t \sqrt{\frac{g}{h_0}} + \frac{4}{f\frac{U(t)^2}{g h_0}} \left( 1 - \frac{U(t)}{2 \sqrt{g h_0}} \right)^4
2260
+
2261
+ with :math:`U(t)` the front wave velocity solution of:
2262
+
2263
+ .. math::
2264
+ \left( \frac{U}{\sqrt{g h_0}} \right)^3 - 8 \left( 0.75 - \frac{3 f t \sqrt{g}}{8 \sqrt{h_0}} \right) \left( \frac{U}{\sqrt{g h_0}} \right)^2 + 12 \left( \frac{U}{\sqrt{g h_0}} \right) - 8 = 0
2265
+
2266
+ Chanson, Hubert. (2005). Analytical Solution of Dam Break Wave with Flow Resistance: Application to Tsunami Surges. 137.
2267
+
2268
+ Parameters
2269
+ ----------
2270
+ t : float
2271
+ Time instant.
2272
+ f : float
2273
+ Darcy friction coefficient.
2274
+
2275
+ Returns
2276
+ -------
2277
+ float
2278
+ Position of the front edge of the fluid.
2279
+ """
2280
+ coeffs = [1, (-8*(0.75 - ((3 * f * t * np.sqrt(self._g)) / (8 * np.sqrt(self._h0))))), 12, -8]
2281
+ roots = np.roots(coeffs)
2282
+
2283
+ real_root = roots[-1].real
2284
+ cf = real_root * np.sqrt(self._g * self._h0)
2285
+
2286
+ term1 = ((1.5 * (cf / np.sqrt(self._g * self._h0))) - 1) * np.sqrt(self._g / self._h0) * t
2287
+ term2 = (4 / (f * ((cf**2) / (self._g * self._h0)))) * (1 - 0.5 * (cf / np.sqrt(self._g * self._h0)))**4
2288
+
2289
+ xf = self._h0 * (term1 + term2)
2290
+
2291
+ if t in self._labels:
2292
+ if "Chanson" not in self._labels[t] :
2293
+ self._labels[t].append("Chanson")
2294
+ self._xf[t].append(xf)
2295
+ else:
2296
+ self._labels[t] = ["Chanson"]
2297
+ self._xf[t] = [xf]
2298
+
2299
+ return xf
2300
+
2301
+
2302
+ def compute_cf(self, t: float) -> float:
2303
+ r"""Compute the celerity of the wave front by resolving:
2304
+
2305
+
2306
+ Parameters
2307
+ ----------
2308
+ t : float
2309
+ Time instant
2310
+
2311
+ Returns
2312
+ -------
2313
+ float
2314
+ Value of the front wave velocity.
2315
+ """
2316
+
2317
+
2318
+ def show_fronts_over_methods(self, x_unit: str="m") -> None:
2319
+ """Plot the front distance from the initial position for each method.
2320
+
2321
+ Parameters
2322
+ ----------
2323
+ x_unit : str, optional
2324
+ X-axis unit, by default "m"
2325
+ """
2326
+ fig, ax = plt.subplots(figsize=(10, 5))
2327
+
2328
+ label_order = []
2329
+ for t in sorted(self._labels.keys()):
2330
+ for label in self._labels[t]:
2331
+ if label not in label_order:
2332
+ label_order.append(label)
2333
+
2334
+ y_levels = {label: i for i, label in enumerate(reversed(label_order))}
2335
+ yticks = list(y_levels.values())
2336
+ yticklabels = list(reversed(label_order))
2337
+
2338
+ sorted_times = sorted(self._xf.keys())
2339
+ colors = cm.copper(np.linspace(0, 1, len(sorted_times)))
2340
+
2341
+ for color, t in zip(colors, sorted_times):
2342
+ x_list = self._xf[t]
2343
+ label_list = self._labels[t]
2344
+
2345
+ for x, label in zip(x_list, label_list):
2346
+ y = y_levels[label]
2347
+ ax.vlines(x, y - 0.3, y + 0.3, color=color, linewidth=2)
2348
+ ax.text(x + 1, y, f"{x:.2f}", rotation=90, va='center', fontsize=8, color=color)
2349
+
2350
+ ax.set_yticks(yticks)
2351
+ ax.set_yticklabels(yticklabels)
2352
+ ax.invert_yaxis()
2353
+
2354
+ ax.set_xlim(left=0)
2355
+ ax.set_xlim(right=max(x for sublist in self._xf.values() for x in sublist) + 5)
2356
+
2357
+ ax.set_xlabel(f"x [{x_unit}]")
2358
+ ax.set_title("Flow front positions over time")
2359
+
2360
+ ax.grid(True, axis='x')
2361
+
2362
+ from matplotlib.lines import Line2D
2363
+ legend_elements = [
2364
+ Line2D([0], [0], color=color, lw=2, label=f"t = {t}s")
2365
+ for color, t in zip(colors, sorted_times)
2366
+ ]
2367
+ ax.legend(handles=legend_elements, title="Time steps", loc="best")
2368
+
2369
+ plt.tight_layout()
2370
+ plt.show()
2371
+
2372
+
2373
+ def show_fronts_over_time(self, x_unit: str="m") -> None:
2374
+ """Plot the front distance from the initial position over time for each method.
2375
+
2376
+ Parameters
2377
+ ----------
2378
+ x_unit : str, optional
2379
+ X-axis unit, by default "m"
2380
+ """
2381
+ T = sorted(self._labels.keys())
2382
+
2383
+ dico_xf = {}
2384
+ dico_time = {}
2385
+ for t in T:
2386
+ for i in range(len(self._labels[t])):
2387
+ if self._labels[t][i] not in dico_xf:
2388
+ dico_xf[self._labels[t][i]] = [self._xf[t][i]]
2389
+ dico_time[self._labels[t][i]] = [t]
2390
+ else:
2391
+ dico_xf[self._labels[t][i]].append(self._xf[t][i])
2392
+ dico_time[self._labels[t][i]].append(t)
2393
+
2394
+ for label in dico_xf.keys():
2395
+ plt.scatter(dico_xf[label], dico_time[label], marker='x', label=label)
2396
+
2397
+ plt.xlabel(f'Distance to the dam break [{x_unit}]')
2398
+ plt.ylabel('Time [s]')
2399
+
2400
+
2401
+ plt.grid(which="major")
2402
+ plt.legend(loc='best')
2403
+ plt.show()