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/read.py ADDED
@@ -0,0 +1,2588 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ from __future__ import annotations
5
+ from abc import abstractmethod
6
+ from typing import Callable
7
+
8
+ import matplotlib
9
+ import matplotlib.pyplot as plt
10
+ import numpy as np
11
+
12
+ import os
13
+ import importlib
14
+ import warnings
15
+
16
+ import pytopomap.plot as plt_fn
17
+ import tilupy.notations as notations
18
+ import tilupy.utils as utils
19
+ import tilupy.plot as plt_tlp
20
+ import tilupy.raster
21
+
22
+
23
+ ALLOWED_MODELS = ["shaltop",
24
+ "lave2D",
25
+ "saval2D"]
26
+ """Allowed models for result reading."""
27
+
28
+
29
+ RAW_STATES = ["hvert", "h", "ux", "uy"]
30
+ """Raw states at the output of a model.
31
+
32
+ Implemented states :
33
+
34
+ - hvert : Fluid thickness taken vertically
35
+ - h : Fluid thickness taken normal to topography
36
+ - ux : X-component of fluid velocity
37
+ - uy : Y-component of fluid velocity
38
+ """
39
+
40
+ TEMPORAL_DATA_0D = ["ek", "volume"]
41
+ """Time-varying 0D data.
42
+
43
+ Implemented 0D temporal data :
44
+
45
+ - ek : kinetic energy
46
+ - volume : Fluid volume
47
+
48
+ Also combine all the assembly possibilities between :data:`TEMPORAL_DATA_2D` and :data:`NP_OPERATORS` (or :data:`OTHER_OPERATORS`), at each point xy following this format:
49
+
50
+ `[TEMPORAL_DATA_2D]_[NP/OTHER_OPERATORS]_xy`
51
+
52
+ For instance with h :
53
+ - h_max_xy: Maximum value of h over the entire surface for each time step.
54
+ - h_min_xy: Minimal value of h over the entire surface for each time step.
55
+ - h_mean_xy: Mean value of h over the entire surface for each time step.
56
+ - h_std_xy: Standard deviation of h over the entire surface for each time step.
57
+ - h_sum_xy: Sum of each value of h at each point of the surface for each time step.
58
+ - h_int_xy: Integrated value of h at each point of the surface for each time step.
59
+ """
60
+
61
+ TEMPORAL_DATA_1D = []
62
+ """Time-varying 1D data.
63
+
64
+ Combine all the assembly possibilities between :data:`TEMPORAL_DATA_2D` and :data:`NP_OPERATORS` (or :data:`OTHER_OPERATORS`) and with an axis like this:
65
+
66
+ `[TEMPORAL_DATA_2D]_[NP/OTHER_OPERATORS]_[x/y]`
67
+
68
+ For instance with h :
69
+ - h_max_x: For each Y coordinate, maximum value of h integrating the values of all points on the X axis (along the fixed Y axis) and integrating all time steps, giving hmax(y).
70
+ - h_max_y: For each X coordinate, maximum value of h integrating the values of all points on the Y axis (along the fixed X axis) and integrating all time steps, giving hmax(x).
71
+ - h_min_x: For each Y coordinate, minimum value of h integrating the values of all points on the X axis (along the fixed Y axis) and integrating all time steps, giving hmin(y).
72
+ - h_min_y: For each X coordinate, minimum value of h integrating the values of all points on the Y axis (along the fixed X axis) and integrating all time steps, giving hmin(x).
73
+ - h_mean_x: For each Y coordinate, mean value of h integrating the values of all points on the X axis (along the fixed Y axis) and integrating all time steps, giving hmean(y).
74
+ - h_mean_y: For each X coordinate, mean value of h integrating the values of all points on the Y axis (along the fixed X axis) and integrating all time steps, giving hmean(x).
75
+ - h_std_x: For each Y coordinate, standard deviation of h integrating the values of all points on the X axis (along the fixed Y axis) and integrating all time steps, giving hstd(y).
76
+ - h_std_y: For each X coordinate, standard deviation of h integrating the values of all points on the Y axis (along the fixed X axis) and integrating all time steps, giving hstd(x).
77
+ - h_sum_x: For each Y coordinate, sum of each value of h integrating the values of all points on the X axis (along the fixed Y axis) and integrating all time steps, giving hsum(y).
78
+ - h_sum_y: For each X coordinate, sum of each value of h integrating the values of all points on the Y axis (along the fixed X axis) and integrating all time steps, giving hsum(x).
79
+ - h_int_x: For each Y coordinate, integrate each value of h integrating the values of all points on the X axis (along the fixed Y axis) and integrating all time steps, giving hint(y).
80
+ - h_int_y: For each X coordinate, integrate each value of h integrating the values of all points on the Y axis (along the fixed X axis) and integrating all time steps, giving hint(x).
81
+ """
82
+
83
+ TEMPORAL_DATA_2D = ["hvert", "h", "u", "ux", "uy", "hu", "hu2"]
84
+ """Time-varying 2D data.
85
+
86
+ Implemented 2D temporal data :
87
+
88
+ - hvert : Fluid height taken vertically
89
+ - h : Fluid height taken normal to topography
90
+ - u : Fluid velocity
91
+ - ux : X-component of fluid velocity
92
+ - uy : Y-component of fluid velocity
93
+ - hu : Momentum flux
94
+ - hu2 : Convective momentum flux
95
+ """
96
+
97
+ STATIC_DATA_0D = []
98
+ """Static 0D data."""
99
+
100
+ STATIC_DATA_1D = []
101
+ """Static 1D data."""
102
+
103
+ STATIC_DATA_2D = []
104
+ """Static 2D data.
105
+
106
+ Combine all the assembly possibilities between :data:`TEMPORAL_DATA_2D` and :data:`NP_OPERATORS` (or :data:`OTHER_OPERATORS`) like this:
107
+
108
+ `[TEMPORAL_DATA_2D]_[NP/OTHER_OPERATORS]`
109
+
110
+ For instance with h :
111
+ - h_max : Maximum value of h at each point on the map, integrating all the time steps.
112
+ - h_min : Minimum value of h at each point on the map, integrating all the time steps.
113
+ - h_mean : Mean value of h at each point on the map, integrating all the time steps.
114
+ - h_std : Standard deviation of h at each point on the map, integrating all the time steps.
115
+ - h_sum : Sum of h at each point on the map, integrating all the time steps.
116
+ - h_final : Value of h at each point on the map, for the last time step.
117
+ - h_init : Value of h at each point on the map, for the first time step.
118
+ - h_int : Integrated value of h at each point on the map, integrating all the time steps.
119
+ """
120
+
121
+ TOPO_DATA_2D = ["z", "zinit", "costh"]
122
+ """Data related to topography.
123
+
124
+ Implemented topographic data :
125
+
126
+ - z : Elevation value of topography
127
+ - zinit : Initial elevation value of topography (same as z if the topography doesn't change during the flow)
128
+ - costh : Cosine of the angle between the vertical and the normal to the relief. Factor to transform vertical height (hvert) into normal height (h).
129
+ """
130
+
131
+ NP_OPERATORS = ["max", "min", "mean", "std", "sum"]
132
+ """Statistical operators.
133
+
134
+ Implemented operators :
135
+
136
+ - max : Maximum
137
+ - min : Minimum
138
+ - mean : Mean
139
+ - std : Standard deviation
140
+ - sum : Sum
141
+ """
142
+
143
+ OTHER_OPERATORS = ["final", "init", "int"]
144
+ """Other operators.
145
+
146
+ Implemented operators :
147
+
148
+ - final : Final value
149
+ - init : Initial value
150
+ - int : Integrated value
151
+ """
152
+
153
+ TIME_OPERATORS = ["final", "init", "int"]
154
+ """Time-related operators.
155
+
156
+ Implemented operators :
157
+
158
+ - final : Final value
159
+ - init : Initial value
160
+ - int : Integrated value
161
+ """
162
+
163
+ for stat in NP_OPERATORS + OTHER_OPERATORS:
164
+ for name in TEMPORAL_DATA_2D:
165
+ STATIC_DATA_2D.append(name + "_" + stat)
166
+
167
+ for stat in NP_OPERATORS + ["int"]:
168
+ for name in TEMPORAL_DATA_2D:
169
+ TEMPORAL_DATA_0D.append(name + "_" + stat + "_xy")
170
+
171
+ for stat in NP_OPERATORS + ["int"]:
172
+ for name in TEMPORAL_DATA_2D:
173
+ for axis in ["x", "y"]:
174
+ TEMPORAL_DATA_1D.append(name + "_" + stat + "_" + axis)
175
+
176
+
177
+ TEMPORAL_DATA = TEMPORAL_DATA_0D + TEMPORAL_DATA_1D + TEMPORAL_DATA_2D
178
+ """Assembling all temporal data.
179
+
180
+ :data:`TEMPORAL_DATA_0D` + :data:`TEMPORAL_DATA_1D` + :data:`TEMPORAL_DATA_2D`
181
+ """
182
+
183
+ STATIC_DATA = STATIC_DATA_0D + STATIC_DATA_1D + STATIC_DATA_2D
184
+ """Assembling all static data.
185
+
186
+ :data:`STATIC_DATA_0D` + :data:`STATIC_DATA_1D` + :data:`STATIC_DATA_2D`
187
+ """
188
+
189
+ DATA_NAMES = TEMPORAL_DATA_0D + TEMPORAL_DATA_1D + TEMPORAL_DATA_2D + STATIC_DATA_0D + STATIC_DATA_1D + STATIC_DATA_2D
190
+ """Assembling all data.
191
+
192
+ :data:`TEMPORAL_DATA` + :data:`STATIC_DATA`
193
+ """
194
+
195
+
196
+ class AbstractResults:
197
+ """Abstract class for :class:`tilupy.read.TemporalResults` and :class:`tilupy.read.StaticResults`.
198
+
199
+ Parameters
200
+ ----------
201
+ name : str
202
+ Name of the property.
203
+ d : numpy.ndarray
204
+ Values of the property.
205
+ notation : dict, optional
206
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
207
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
208
+ kwargs
209
+
210
+ Attributes
211
+ ----------
212
+ _name : str
213
+ Name of the property.
214
+ _d : numpy.ndarray
215
+ Values of the property.
216
+ _notation : tilupy.notations.Notation
217
+ Instance of the class :class:`tilupy.notations.Notation`.
218
+ """
219
+ def __init__(self, name: str,
220
+ d: np.ndarray,
221
+ notation: dict = None,
222
+ **kwargs):
223
+ self._name = name
224
+ self._d = d
225
+ if isinstance(notation, dict):
226
+ self._notation = notations.Notation(**notation)
227
+ elif notation is None:
228
+ self._notation = notations.get_notation(name)
229
+ else:
230
+ self._notation = notation
231
+ self.__dict__.update(kwargs)
232
+
233
+
234
+ @property
235
+ def d(self) -> np.ndarray:
236
+ """Get data values.
237
+
238
+ Returns
239
+ -------
240
+ np.ndarray
241
+ Attribute :attr:`_d`.
242
+ """
243
+ return self._d
244
+
245
+
246
+ @property
247
+ def name(self) -> str:
248
+ """Get data name.
249
+
250
+ Returns
251
+ -------
252
+ str
253
+ Attribute :attr:`_name`.
254
+ """
255
+ return self._name
256
+
257
+
258
+ @property
259
+ def notation(self) -> tilupy.notations.Notation:
260
+ """Get data notation.
261
+
262
+ Returns
263
+ -------
264
+ tilupy.notations.Notation
265
+ Attribute :attr:`_notation`.
266
+ """
267
+ return self._notation
268
+
269
+
270
+ class TemporalResults(AbstractResults):
271
+ """Abstract class for time dependent result of simulation.
272
+
273
+ Parameters
274
+ ----------
275
+ name : str
276
+ Name of the property.
277
+ d : numpy.ndarray
278
+ Values of the property.
279
+ t : numpy.ndarray
280
+ Time steps, must match the last dimension of :data:`d`.
281
+ notation : dict, optional
282
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
283
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
284
+
285
+ Attributes
286
+ ----------
287
+ _name : str
288
+ Name of the property.
289
+ _d : numpy.ndarray
290
+ Values of the property.
291
+ _notation : tilupy.notations.Notation
292
+ Instance of the class :class:`tilupy.notations.Notation`.
293
+ _t : numpy.ndarray
294
+ Time steps.
295
+ """
296
+ def __init__(self, name: str, d: np.ndarray, t: np.ndarray, notation: dict=None):
297
+ super().__init__(name, d, notation=notation)
298
+ # 1d array with times, matching last dimension of self.d
299
+ self._t = t
300
+
301
+
302
+ def get_temporal_stat(self, stat: str) -> tilupy.read.StaticResults2D | tilupy.read.StaticResults1D:
303
+ """Statistical analysis along temporal dimension.
304
+
305
+ Parameters
306
+ ----------
307
+ stat : str
308
+ Statistical operator to apply. Must be implemented in :data:`NP_OPERATORS` or in
309
+ :data:`OTHER_OPERATORS`.
310
+
311
+ Returns
312
+ -------
313
+ tilupy.read.StaticResults2D or tilupy.read.StaticResults1D
314
+ Static result object depending on the dimensionality of the data.
315
+ """
316
+ if stat in NP_OPERATORS:
317
+ dnew = getattr(np, stat)(self._d, axis=-1)
318
+ elif stat == "final":
319
+ dnew = self._d[..., -1]
320
+ elif stat == "init":
321
+ dnew = self._d[..., 0]
322
+ elif stat == "int":
323
+ dnew = np.trapezoid(self._d, x=self._t)
324
+
325
+ notation = notations.add_operator(self._notation, stat, axis="t")
326
+
327
+ if dnew.ndim == 2:
328
+ return StaticResults2D(self._name + "_" + stat,
329
+ dnew,
330
+ notation=notation,
331
+ x=self.x,
332
+ y=self.y,
333
+ z=self.z,
334
+ )
335
+ elif dnew.ndim == 1:
336
+ return StaticResults1D(self._name + "_" + stat,
337
+ dnew,
338
+ notation=notation,
339
+ coords=self._coords,
340
+ coords_name=self._coords_name,
341
+ )
342
+
343
+
344
+ @abstractmethod
345
+ def get_spatial_stat(self, stat, axis):
346
+ """Abstract method for statistical analysis along spatial dimension.
347
+
348
+ Parameters
349
+ ----------
350
+ stat : str
351
+ Statistical operator to apply. Must be implemented in :data:`NP_OPERATORS` or in
352
+ :data:`OTHER_OPERATORS`.
353
+ axis : str
354
+ Axis where to do the analysis.
355
+ """
356
+ pass
357
+
358
+
359
+ @abstractmethod
360
+ def plot(*arg, **kwargs):
361
+ """Abstract method to plot the temporal evolution of the results."""
362
+ pass
363
+
364
+
365
+ @abstractmethod
366
+ def save(*arg, **kwargs):
367
+ """Abstract method to save the temporal results."""
368
+ pass
369
+
370
+
371
+ @abstractmethod
372
+ def extract_from_time_step(*arg, **kwargs):
373
+ """Abstract method to extract data from specific time steps of a TemporalResults."""
374
+ pass
375
+
376
+
377
+ @property
378
+ def t(self) -> np.ndarray:
379
+ """Get times.
380
+
381
+ Returns
382
+ -------
383
+ numpy.ndarray
384
+ Attribute :attr:`_t`
385
+ """
386
+ return self._t
387
+
388
+
389
+ class TemporalResults0D(TemporalResults):
390
+ """
391
+ Class for simulation results described where the data is one or multiple scalar functions of time.
392
+
393
+ Parameters
394
+ ----------
395
+ name : str
396
+ Name of the property.
397
+ d : numpy.ndarray
398
+ Values of the property. The last dimension correspond to time.
399
+ It can be a one dimensionnal Nt array, or a two dimensionnal [Nd, Nt] array,
400
+ where Nt is the legnth of :data:`t`, and Nd correspond to the number of scalar values
401
+ of interest (e.g. X and Y coordinates of the center of mass / front).
402
+ t : numpy.ndarray
403
+ Time steps, must match the last dimension of :data:`d` (size Nt).
404
+ scalar_names : list[str]
405
+ List of length Nd containing the names of the scalar fields (one
406
+ name per row of d)
407
+ notation : dict, optional
408
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
409
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
410
+
411
+ Attributes
412
+ ----------
413
+ _name : str
414
+ Name of the property.
415
+ _d : numpy.ndarray
416
+ Values of the property.
417
+ _notation : tilupy.notations.Notation
418
+ Instance of the class :class:`tilupy.notations.Notation`.
419
+ _t : numpy.ndarray
420
+ Time steps.
421
+ _scalar_names : list[str]
422
+ List of names of the scalar fields.
423
+ """
424
+
425
+ def __init__(self, name: str,
426
+ d: np.ndarray,
427
+ t: np.ndarray,
428
+ scalar_names: list[str]=None,
429
+ notation: dict=None):
430
+ super().__init__(name, d, t, notation=notation)
431
+ self._scalar_names = scalar_names
432
+
433
+
434
+ def plot(self,
435
+ ax: matplotlib.axes._axes.Axes=None,
436
+ figsize: tuple[float]=None,
437
+ **kwargs
438
+ ) -> matplotlib.axes._axes.Axes:
439
+ """Plot the temporal evolution of the 0D results.
440
+
441
+ Parameters
442
+ ----------
443
+ ax : matplotlib.axes._axes.Axes, optional
444
+ Existing matplotlib window, if None create one. By default None.
445
+ figsize : tuple[float], optional
446
+ Size of the figure, by default None.
447
+
448
+ Returns
449
+ -------
450
+ matplotlib.axes._axes.Axes
451
+ The created plot.
452
+ """
453
+ if ax is None:
454
+ fig, ax = plt.subplots(1, 1, figsize=figsize, layout="constrained")
455
+
456
+ if isinstance(self._d, np.ndarray):
457
+ data = self._d.T
458
+ else:
459
+ data = self._d
460
+
461
+ if "color" not in kwargs and self._scalar_names is None:
462
+ color = "black"
463
+ kwargs["color"] = color
464
+
465
+ ax.plot(self._t, data, label=self._scalar_names, **kwargs) # Remove label=self._scalar_names
466
+
467
+ ax.grid(True, alpha=0.3)
468
+ ax.set_xlim(left=min(self._t), right=max(self._t))
469
+ ax.set_xlabel(notations.get_label("t"))
470
+ ax.set_ylabel(notations.get_label(self._notation))
471
+
472
+ return ax
473
+
474
+
475
+ def save(self):
476
+ """Save the temporal 0D results.
477
+
478
+ Raises
479
+ ------
480
+ NotImplementedError
481
+ Not implemented yet.
482
+ """
483
+ raise NotImplementedError("Saving method for :class:`tilupy.read.TemporalResults0D` not implemented yet")
484
+
485
+
486
+ def get_spatial_stat(self, *arg, **kwargs):
487
+ """Statistical analysis along spatial dimension for 0D results.
488
+
489
+ Raises
490
+ ------
491
+ NotImplementedError
492
+ Not implemented because irrelevant.
493
+ """
494
+ raise NotImplementedError("Spatial integration of :class:`tilupy.read.Spatialresults0D` is not implemented because non relevant")
495
+
496
+
497
+ def extract_from_time_step(self,
498
+ time_steps: float | list[float],
499
+ ) -> tilupy.read.StaticResults0D | tilupy.read.TemporalResults0D:
500
+ """Extract data from specific time steps.
501
+
502
+ Parameters
503
+ ----------
504
+ time_steps : float | list[float]
505
+ Value of time steps to extract data.
506
+
507
+ Returns
508
+ -------
509
+ tilupy.read.StaticResults0D | tilupy.read.TemporalResults0D
510
+ Extracted data, the type depends on the time step.
511
+ """
512
+ if isinstance(time_steps, float) or isinstance(time_steps, int):
513
+ t_index = np.argmin(np.abs(self._t - time_steps))
514
+
515
+ return StaticResults0D(name=self._name,
516
+ d=self._d[t_index],
517
+ notation=self._notation)
518
+
519
+ elif isinstance(time_steps, list):
520
+ time_steps = np.array(time_steps)
521
+
522
+ if isinstance(time_steps, np.ndarray):
523
+ if isinstance(self._t, list):
524
+ self._t = np.array(self._t)
525
+ t_distances = np.abs(self._t[None, :] - time_steps[:, None])
526
+ t_indexes = np.argmin(t_distances, axis=1)
527
+
528
+ return TemporalResults0D(name=self._name,
529
+ d=self._d[t_indexes],
530
+ t=self._t[t_indexes],
531
+ notation=self._notation)
532
+
533
+
534
+ @property
535
+ def scalar_names(self) -> list[str]:
536
+ """Get list of names of the scalar fields.
537
+
538
+ Returns
539
+ -------
540
+ list[str]
541
+ Attribute :attr:`_scalar_names`.
542
+ """
543
+ return self._scalar_names
544
+
545
+
546
+ class TemporalResults1D(TemporalResults):
547
+ """
548
+ Class for simulation results described by one dimension for space and one dimension for time.
549
+
550
+ Parameters
551
+ ----------
552
+ name : str
553
+ Name of the property.
554
+ d : numpy.ndarray
555
+ Values of the property. The last dimension correspond to time.
556
+ It can be a one dimensionnal Nt array, or a two dimensionnal [Nd, Nt] array,
557
+ where Nt is the legnth of :data:`t`, and Nd correspond to the number of scalar values
558
+ of interest (e.g. X and Y coordinates of the center of mass / front).
559
+ t : numpy.ndarray
560
+ Time steps, must match the last dimension of :data:`d` (size Nt).
561
+ coords: numpy.ndarray
562
+ Spatial coordinates.
563
+ coords_name: str
564
+ Spatial coordinates name.
565
+ notation : dict, optional
566
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
567
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
568
+
569
+ Attributes
570
+ ----------
571
+ _name : str
572
+ Name of the property.
573
+ _d : numpy.ndarray
574
+ Values of the property.
575
+ _notation : tilupy.notations.Notation
576
+ Instance of the class :class:`tilupy.notations.Notation`.
577
+ _t : numpy.ndarray
578
+ Time steps.
579
+ _coords: numpy.ndarray
580
+ Spatial coordinates.
581
+ _coords_name: str
582
+ Spatial coordinates name.
583
+ """
584
+ def __init__(self, name: str,
585
+ d: np.ndarray,
586
+ t: np.ndarray,
587
+ coords: np.ndarray=None,
588
+ coords_name: str=None,
589
+ notation: dict=None):
590
+ super().__init__(name, d, t, notation=notation)
591
+ # x and y arrays
592
+ self._coords = coords
593
+ self._coords_name = coords_name
594
+
595
+
596
+ def plot(self,
597
+ coords = None,
598
+ plot_type: str = "simple",
599
+ figsize: tuple[float] = None,
600
+ ax: matplotlib.axes._axes.Axes = None,
601
+ linestyles: list[str] = None,
602
+ cmap: str = 'viridis',
603
+ highlighted_curve: bool = False,
604
+ **kwargs
605
+ ) -> matplotlib.axes._axes.Axes:
606
+ """Plot the temporal evolution of the 1D results.
607
+
608
+ Parameters
609
+ ----------
610
+ coords : numpy.ndarray, optional
611
+ Specified coordinates, if None uses the coordinates implemented when creating the instance (:attr:`_coords`).
612
+ By default None.
613
+ plot_type: str, optional
614
+ Wanted plot :
615
+
616
+ - "simple" : Every curve in the same graph
617
+ - "multiples" : Every curve in separate graph
618
+ - "'shotgather" : Shotgather graph
619
+
620
+ By default "simple".
621
+ ax : matplotlib.axes._axes.Axes, optional
622
+ Existing matplotlib window, if None create one. By default None
623
+ linestyles : list[str], optional
624
+ Custom linestyles for each time step. If None, colors and styles are auto-assigned.
625
+ Used only for "simple". By default None.
626
+ cmap : str, optional
627
+ Color map for the ploted curves. Used only for "simple". By default "viridis".
628
+ hightlighted_curved : bool, optional
629
+ Option to display all time steps on each graph of the multiples and
630
+ highlight the curve corresponding to the time step of the subgraph. Used only for "multiples".
631
+ By default False.
632
+ kwargs: dict, optional
633
+ Additional arguments for plot functions.
634
+
635
+ Returns
636
+ -------
637
+ matplotlib.axes._axes.Axes
638
+ The created plot.
639
+
640
+ Raises
641
+ ------
642
+ TypeError
643
+ If missing coordinates.
644
+ """
645
+ if coords is None:
646
+ coords = self._coords
647
+ if coords is None:
648
+ raise TypeError("coords data missing")
649
+
650
+ if ax is None and plot_type != "multiples":
651
+ fig, ax = plt.subplots(1, 1, figsize=figsize, layout="constrained")
652
+
653
+ if plot_type == "shotgather":
654
+ xlabel = notations.get_label(self._coords_name, with_unit=True)
655
+
656
+ if "colorbar_kwargs" not in kwargs:
657
+ kwargs["colorbar_kwargs"] = dict()
658
+ if "label" not in kwargs["colorbar_kwargs"]:
659
+ clabel = notations.get_label(self._notation)
660
+ kwargs["colorbar_kwargs"]["label"] = clabel
661
+
662
+ ax = plt_tlp.plot_shotgather(self._coords,
663
+ self._t,
664
+ self._d,
665
+ xlabel=xlabel,
666
+ ylabel=notations.get_label("t"),
667
+ **kwargs)
668
+
669
+ if plot_type == "simple":
670
+ if linestyles is None or len(linestyles)!=(len(self._t)):
671
+ norm = plt.Normalize(vmin=0, vmax=len(self._t)-1)
672
+ cmap = plt.get_cmap(cmap)
673
+
674
+ for i in range(self._d.shape[1]):
675
+ if linestyles is None or len(linestyles)!=(len(self._t)):
676
+ color = cmap(norm(i)) if self._t[i] != 0 else "red"
677
+ l_style = "-" if self._t[i] != 0 else (0, (1, 4))
678
+ else:
679
+ color = "black" if self._t[i] != 0 else "red"
680
+ l_style = linestyles[i] if self._t[i] != 0 else (0, (1, 4))
681
+
682
+ ax.plot(self._coords, self._d[:, i], label=f"t={self._t[i]}s", color=color, linestyle=l_style, **kwargs)
683
+
684
+ ax.grid(True, alpha=0.3)
685
+ ax.set_xlim(left=min(self._coords), right=max(self._coords))
686
+
687
+ ax.set_xlabel(notations.get_label(self._coords_name))
688
+ ax.set_ylabel(notations.get_label(self._notation))
689
+
690
+ if plot_type == "multiples":
691
+ cols_nb = 3
692
+ if len(self._t) < 3:
693
+ cols_nb = len(self._t)
694
+
695
+ row_nb = len(self._t) // 3
696
+ if len(self._t) % 3 != 0:
697
+ row_nb += 1
698
+
699
+ fig, axes = plt.subplots(nrows=row_nb,
700
+ ncols=cols_nb,
701
+ figsize=figsize,
702
+ layout="constrained",
703
+ sharex=True,
704
+ sharey=True)
705
+ axes = axes.flatten()
706
+
707
+ for i in range(self._d.shape[1]):
708
+ if highlighted_curve:
709
+ for j in range(self._d.shape[1]):
710
+ if i == j:
711
+ axes[i].plot(self._coords, self._d[:, j], color="black", linewidth=1.5, **kwargs)
712
+ else:
713
+ axes[i].plot(self._coords, self._d[:, j], color="gray", alpha=0.5, linewidth=0.5, **kwargs)
714
+ else:
715
+ axes[i].plot(self._coords, self._d[:, i], color="black", **kwargs)
716
+
717
+ axes[i].grid(True, alpha=0.3)
718
+ axes[i].set_xlim(left=min(self._coords), right=max(self._coords))
719
+
720
+ axes[i].set_xlabel(notations.get_label(self._coords_name))
721
+ axes[i].set_ylabel(notations.get_label(self._notation))
722
+
723
+ axes[i].set_title(f"t={self._t[i]}s", loc='left')
724
+
725
+ for i in range(len(self._t), len(axes)):
726
+ fig.delaxes(axes[i])
727
+
728
+ return axes
729
+
730
+ return ax
731
+
732
+
733
+ def save(self):
734
+ """Save the temporal 1D results.
735
+
736
+ Raises
737
+ ------
738
+ NotImplementedError
739
+ Not implemented yet.
740
+ """
741
+ raise NotImplementedError("Saving method for TemporalResults1D not implemented yet")
742
+
743
+
744
+ def get_spatial_stat(self, stat, **kwargs) -> tilupy.read.TemporalResults0D:
745
+ """Statistical analysis along spatial dimension for 1D results.
746
+
747
+ Parameters
748
+ ----------
749
+ stat : str
750
+ Statistical operator to apply. Must be implemented in :data:`NP_OPERATORS` or in
751
+ :data:`OTHER_OPERATORS`.
752
+
753
+ Returns
754
+ -------
755
+ tilupy.read.TemporalResults0D
756
+ Instance of :class:`tilupy.read.TemporalResults0D`.
757
+ """
758
+ if stat in NP_OPERATORS:
759
+ dnew = getattr(np, stat)(self._d, axis=0)
760
+ elif stat == "int":
761
+ dd = self._coords[1] - self._coords[0]
762
+ dnew = np.sum(self._d, axis=0) * dd
763
+ notation = notations.add_operator(self._notation, stat, axis=self._coords_name)
764
+
765
+ return TemporalResults0D(self._name + "_" + stat,
766
+ dnew,
767
+ self._t,
768
+ notation=notation)
769
+
770
+
771
+ def extract_from_time_step(self,
772
+ time_steps: float | list[float],
773
+ ) -> tilupy.read.StaticResults1D | tilupy.read.TemporalResults1D:
774
+ """Extract data from specific time steps.
775
+
776
+ Parameters
777
+ ----------
778
+ time_steps : float | list[float]
779
+ Value of time steps to extract data.
780
+
781
+ Returns
782
+ -------
783
+ tilupy.read.StaticResults1D | tilupy.read.TemporalResults1D
784
+ Extracted data, the type depends on the time step.
785
+ """
786
+ if isinstance(time_steps, float) or isinstance(time_steps, int):
787
+ t_index = np.argmin(np.abs(self._t - time_steps))
788
+
789
+ return StaticResults1D(name=self._name,
790
+ d=self._d[:, t_index],
791
+ coords=self._coords,
792
+ coords_name=self._coords_name,
793
+ notation=self._notation)
794
+
795
+ elif isinstance(time_steps, list):
796
+ time_steps = np.array(time_steps)
797
+
798
+ if isinstance(time_steps, np.ndarray):
799
+ if isinstance(self._t, list):
800
+ self._t = np.array(self._t)
801
+ t_distances = np.abs(self._t[None, :] - time_steps[:, None])
802
+ t_indexes = np.argmin(t_distances, axis=1)
803
+
804
+ return TemporalResults1D(name=self._name,
805
+ d=self._d[:, t_indexes],
806
+ t=self._t[t_indexes],
807
+ coords=self._coords,
808
+ coords_name=self._coords_name,
809
+ notation=self._notation)
810
+
811
+
812
+ @property
813
+ def coords(self) -> np.ndarray:
814
+ """Get spatial coordinates.
815
+
816
+ Returns
817
+ -------
818
+ numpy.ndarray
819
+ Attribute :attr:`_coords`
820
+ """
821
+ return self._coords
822
+
823
+
824
+ @property
825
+ def coords_name(self) -> str:
826
+ """Get spatial coordinates name.
827
+
828
+ Returns
829
+ -------
830
+ str
831
+ Attribute :attr:`_coords_name`
832
+ """
833
+ return self._coords_name
834
+
835
+
836
+ class TemporalResults2D(TemporalResults):
837
+ """
838
+ Class for simulation results described by a two dimensional space and one dimension for time.
839
+
840
+ Parameters
841
+ ----------
842
+ name : str
843
+ Name of the property.
844
+ d : numpy.ndarray
845
+ Values of the property. The last dimension correspond to time.
846
+ It can be a one dimensionnal Nt array, or a two dimensionnal [Nd, Nt] array,
847
+ where Nt is the legnth of :data:`t`, and Nd correspond to the number of scalar values
848
+ of interest (e.g. X and Y coordinates of the center of mass / front).
849
+ t : numpy.ndarray
850
+ Time steps, must match the last dimension of :data:`d` (size Nt).
851
+ x : numpy.ndarray
852
+ X coordinate values.
853
+ y : numpy.ndarray
854
+ X coordinate values.
855
+ z : numpy.ndarray
856
+ Elevation values of the surface.
857
+ notation : dict, optional
858
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notation.Notation`.
859
+ If None use the function :func:`tilupy.notation.get_notation`. By default None.
860
+
861
+ Attributes
862
+ ----------
863
+ _name : str
864
+ Name of the property.
865
+ _d : numpy.ndarray
866
+ Values of the property.
867
+ _notation : tilupy.notations.Notation
868
+ Instance of the class :class:`tilupy.notations.Notation`.
869
+ _t : numpy.ndarray
870
+ Time steps.
871
+ _x : numpy.ndarray
872
+ X coordinate values.
873
+ _y : numpy.ndarray
874
+ X coordinate values.
875
+ _z : numpy.ndarray
876
+ Elevation values of the surface.
877
+ """
878
+ def __init__(self,
879
+ name: str,
880
+ d: np.ndarray,
881
+ t: np.ndarray,
882
+ x: np.ndarray=None,
883
+ y: np.ndarray=None,
884
+ z: np.ndarray=None,
885
+ notation: dict=None):
886
+ super().__init__(name, d, t, notation=notation)
887
+ # x and y arrays
888
+ self._x = x
889
+ self._y = y
890
+ # topography
891
+ self._z = z
892
+
893
+
894
+ def plot(self,
895
+ x: np.ndarray = None,
896
+ y: np.ndarray = None,
897
+ z: np.ndarray = None,
898
+ plot_multiples: bool = False,
899
+ file_name: str = None,
900
+ folder_out: str = None,
901
+ figsize: tuple[float] = None,
902
+ dpi: int = None,
903
+ fmt: str = "png",
904
+ sup_plt_fn = None,
905
+ sup_plt_fn_args = None,
906
+ **kwargs
907
+ ) -> None:
908
+ """Plot the temporal evolution of the 2D results using :func:`pytopomap.plot.plot_maps`.
909
+
910
+ Parameters
911
+ ----------
912
+ x : numpy.ndarray, optional
913
+ X coordinate values, if None use :attr:`_x`. By default None.
914
+ y : numpy.ndarray, optional
915
+ Y coordinate values, if None use :attr:`_y`. By default None.
916
+ z : numpy.ndarray, optional
917
+ Elevation values, if None use :attr:`_z`. By default None.
918
+ file_name : str, optional
919
+ Base name for the output image files, by default None.
920
+ folder_out : str, optional
921
+ Path to the output folder. If not provides, figures are not saved. By default None.
922
+ figsize : tuple[float], optional
923
+ Size of the figure, by default None.
924
+ dpi : int, optional
925
+ Resolution for saved figures. Only used if :data:`folder_out` is set. By default None.
926
+ fmt : str, optional
927
+ File format for saving figures, by default "png".
928
+ sup_plt_fn : callable, optional
929
+ A custom function to apply additional plotting on the axes, by default None.
930
+ sup_plt_fn_args : dict, optional
931
+ Arguments to pass to :data:`sup_plt_fn`, by default None.
932
+
933
+ Raises
934
+ ------
935
+ TypeError
936
+ If no value for x, y.
937
+ """
938
+ if file_name is None:
939
+ file_name = self._name
940
+
941
+ if x is None:
942
+ x = self._x
943
+ if y is None:
944
+ y = self._y
945
+ if z is None:
946
+ z = self._z
947
+
948
+ if x is None or y is None:
949
+ raise TypeError("x, y or z data missing")
950
+
951
+ if z is None:
952
+ warnings.warn("No topography given.")
953
+
954
+ if "colorbar_kwargs" not in kwargs:
955
+ kwargs["colorbar_kwargs"] = dict()
956
+ if "label" not in kwargs["colorbar_kwargs"]:
957
+ clabel = notations.get_label(self._notation)
958
+ kwargs["colorbar_kwargs"]["label"] = clabel
959
+
960
+ if plot_multiples:
961
+ if "vmin" not in kwargs:
962
+ kwargs["vmin"] = np.min(self._d)
963
+ if "vmax" not in kwargs:
964
+ kwargs["vmax"] = np.max(self._d)
965
+
966
+ cols_nb = 3
967
+ if len(self._t) < 3:
968
+ cols_nb = len(self._t)
969
+
970
+ row_nb = len(self._t) // 3
971
+ if len(self._t) % 3 != 0:
972
+ row_nb += 1
973
+
974
+ fig, axes = plt.subplots(nrows=row_nb,
975
+ ncols=cols_nb,
976
+ figsize=figsize,
977
+ layout="constrained",
978
+ sharex=True,
979
+ sharey=True)
980
+ axes = axes.flatten()
981
+
982
+ for i in range(len(self._t)):
983
+ plt_fn.plot_data_on_topo(x=x,
984
+ y=y,
985
+ z=z,
986
+ data=self._d[:, :, i],
987
+ axe=axes[i],
988
+ plot_colorbar=False,
989
+ **kwargs)
990
+
991
+ axes[i].set_title(f"t={self._t[i]}s", loc='left')
992
+
993
+ for i in range(len(self._t), len(axes)):
994
+ fig.delaxes(axes[i])
995
+
996
+ max_val, idx = 0, 0
997
+ for i in range(len(self._t)):
998
+ max_val_t = np.max(axes[i].images[1].get_array())
999
+ if max_val_t > max_val:
1000
+ max_val = max_val_t
1001
+ idx = i
1002
+ mappable = axes[idx].images[1]
1003
+ fig.colorbar(mappable, ax=axes, orientation='vertical', **kwargs["colorbar_kwargs"])
1004
+
1005
+ return axes
1006
+
1007
+ plt_fn.plot_maps(x,
1008
+ y,
1009
+ z,
1010
+ self._d,
1011
+ self._t,
1012
+ file_name=file_name,
1013
+ folder_out=folder_out,
1014
+ figsize=figsize,
1015
+ dpi=dpi,
1016
+ fmt=fmt,
1017
+ sup_plt_fn=sup_plt_fn,
1018
+ sup_plt_fn_args=sup_plt_fn_args,
1019
+ **kwargs
1020
+ )
1021
+
1022
+ return None
1023
+
1024
+
1025
+ def save(self,
1026
+ folder: str=None,
1027
+ file_name: str=None,
1028
+ fmt: str="asc",
1029
+ time: str | int=None,
1030
+ x: np.ndarray=None,
1031
+ y: np.ndarray=None,
1032
+ **kwargs
1033
+ ) -> None:
1034
+ """Save the temporal 2D results.
1035
+
1036
+ Parameters
1037
+ ----------
1038
+ folder : str, optional
1039
+ Path to the output folder, if None create a folder with :attr:`_name`. By default None.
1040
+ file_name : str, optional
1041
+ Base name for the output image files, if None use :attr:`_name`. By default None.
1042
+ fmt : str, optional
1043
+ File format for saving result, by default "asc".
1044
+ time : str | int, optional
1045
+ Time instants to save the results.
1046
+
1047
+ - If time is string, must be "initial" or "final".
1048
+ - If time is int, used as index in :attr:`_t`.
1049
+ - If None use every instant in :attr:`_t`.
1050
+
1051
+ By default None.
1052
+ x : np.ndarray, optional
1053
+ X coordinate values, if None use :attr:`_x`. By default None.
1054
+ y : np.ndarray, optional
1055
+ Y coordinate values, if None use :attr:`_y`. By default None.
1056
+
1057
+ Returns
1058
+ -------
1059
+ None
1060
+
1061
+ Raises
1062
+ ------
1063
+ ValueError
1064
+ If no value for x, y.
1065
+ """
1066
+ if x is None:
1067
+ x = self._x
1068
+ if y is None:
1069
+ y = self._y
1070
+ if x is None or y is None:
1071
+ raise ValueError("x et y arrays must not be None")
1072
+
1073
+ if file_name is None:
1074
+ file_name = self._name
1075
+
1076
+ if folder is not None:
1077
+ file_name = os.path.join(folder, file_name)
1078
+
1079
+ if time is not None:
1080
+ if isinstance(time, str):
1081
+ if time == "final":
1082
+ inds = [self._d.shape[2] - 1]
1083
+ elif time == "initial":
1084
+ inds = [0]
1085
+ else:
1086
+ inds = [np.argmin(time - np.abs(np.array(self._t) - time))]
1087
+ else:
1088
+ inds = range(len(self._t))
1089
+
1090
+ for i in inds:
1091
+ file_out = file_name + "_{:04d}.".format(i) + fmt
1092
+ tilupy.raster.write_raster(x, y, self._d[:, :, i], file_out, fmt=fmt, **kwargs)
1093
+
1094
+
1095
+ def get_spatial_stat(self,
1096
+ stat: str,
1097
+ axis: str | int | tuple[int]=None
1098
+ ) -> tilupy.read.TemporalResults0D | tilupy.read.TemporalResults1D:
1099
+ """Statistical analysis along spatial dimension for 2D results.
1100
+
1101
+ Parameters
1102
+ ----------
1103
+ stat : str
1104
+ Statistical operator to apply. Must be implemented in :data:`NP_OPERATORS`.
1105
+ axis : tuple[int]
1106
+ Axis where to do the analysis:
1107
+
1108
+ - If axis is string, replace 'x' by 1, 'y' by 0 and 'xy' by (0, 1).
1109
+ - If axis is int, only use 0 or 1.
1110
+ - If None use (0, 1). By default None.
1111
+
1112
+ Returns
1113
+ -------
1114
+ tilupy.read.TemporalResults0D or tilupy.read.TemporalResults1D
1115
+ Instance of :class:`tilupy.read.TemporalResults0D` or :class:`tilupy.read.TemporalResults1D`.
1116
+ """
1117
+ if axis is None:
1118
+ axis = (0, 1)
1119
+
1120
+ if isinstance(axis, str):
1121
+ axis_str = axis
1122
+ if axis == "x":
1123
+ axis = 1
1124
+ elif axis == "y":
1125
+ axis = 0
1126
+ elif axis == "xy":
1127
+ axis = (0, 1)
1128
+ else:
1129
+ if axis == 1:
1130
+ axis_str = "x"
1131
+ elif axis == 0:
1132
+ axis_str = "y"
1133
+ elif axis == (0, 1):
1134
+ axis_str = "xy"
1135
+
1136
+ if stat in NP_OPERATORS:
1137
+ dnew = getattr(np, stat)(self._d, axis=axis)
1138
+ elif stat == "int":
1139
+ dnew = np.sum(self._d, axis=axis)
1140
+ if axis == 1:
1141
+ dd = self._x[1] - self._x[0]
1142
+ elif axis == 0:
1143
+ dd = self._y[1] - self._y[0]
1144
+ elif axis == (0, 1):
1145
+ dd = (self._x[1] - self._x[0]) * (self._y[1] - self._y[0])
1146
+ dnew = dnew * dd
1147
+
1148
+ if axis == 1:
1149
+ # Needed to get correct orinetation as d[0, 0] is the upper corner
1150
+ # of the data, with coordinates x[0], y[-1]
1151
+ dnew = np.flip(dnew, axis=0)
1152
+
1153
+ new_name = self._name + "_" + stat + "_" + axis_str
1154
+ notation = notations.add_operator(self._notation, stat, axis=axis_str)
1155
+
1156
+ if axis == (0, 1):
1157
+ return TemporalResults0D(new_name,
1158
+ dnew,
1159
+ self._t,
1160
+ notation=notation)
1161
+ else:
1162
+ if axis == 0:
1163
+ coords = self._x
1164
+ coords_name = "x"
1165
+ else:
1166
+ coords = self._y
1167
+ coords_name = "y"
1168
+ return TemporalResults1D(new_name,
1169
+ dnew,
1170
+ self._t,
1171
+ coords,
1172
+ coords_name=coords_name,
1173
+ notation=notation)
1174
+
1175
+
1176
+ def extract_from_time_step(self,
1177
+ time_steps: float | list[float],
1178
+ ) -> tilupy.read.StaticResults2D | tilupy.read.TemporalResults2D:
1179
+ """Extract data from specific time steps.
1180
+
1181
+ Parameters
1182
+ ----------
1183
+ time_steps : float | list[float]
1184
+ Value of time steps to extract data.
1185
+
1186
+ Returns
1187
+ -------
1188
+ tilupy.read.StaticResults2D | tilupy.read.TemporalResults2D
1189
+ Extracted data, the type depends on the time step.
1190
+ """
1191
+ if isinstance(time_steps, float) or isinstance(time_steps, int):
1192
+ t_index = np.argmin(np.abs(self._t - time_steps))
1193
+
1194
+ return StaticResults2D(name=self._name,
1195
+ d=self._d[:, :, t_index],
1196
+ x=self._x,
1197
+ y=self._y,
1198
+ z=self._z,
1199
+ notation=self._notation)
1200
+
1201
+ elif isinstance(time_steps, list):
1202
+ time_steps = np.array(time_steps)
1203
+
1204
+ if isinstance(time_steps, np.ndarray):
1205
+ if isinstance(self._t, list):
1206
+ self._t = np.array(self._t)
1207
+ t_distances = np.abs(self._t[None, :] - time_steps[:, None])
1208
+ t_indexes = np.argmin(t_distances, axis=1)
1209
+
1210
+ return TemporalResults2D(name=self._name,
1211
+ d=self._d[:, :, t_indexes],
1212
+ t=self._t[t_indexes],
1213
+ x=self._x,
1214
+ y=self._y,
1215
+ z=self._z,
1216
+ notation=self._notation)
1217
+
1218
+
1219
+ @property
1220
+ def x(self) -> np.ndarray:
1221
+ """Get X coordinates.
1222
+
1223
+ Returns
1224
+ -------
1225
+ numpy.ndarray
1226
+ Attribute :attr:`_x`.
1227
+ """
1228
+ return self._x
1229
+
1230
+
1231
+ @property
1232
+ def y(self) -> np.ndarray:
1233
+ """Get Y coordinates.
1234
+
1235
+ Returns
1236
+ -------
1237
+ numpy.ndarray
1238
+ Attribute :attr:`_y`.
1239
+ """
1240
+ return self._y
1241
+
1242
+
1243
+ @property
1244
+ def z(self) -> np.ndarray:
1245
+ """Get elevations values.
1246
+
1247
+ Returns
1248
+ -------
1249
+ numpy.ndarray
1250
+ Attribute :attr:`_z`.
1251
+ """
1252
+ return self._z
1253
+
1254
+
1255
+ class StaticResults(AbstractResults):
1256
+ """Abstract class for result of simulation without time dependence.
1257
+
1258
+ Parameters
1259
+ ----------
1260
+ name : str
1261
+ Name of the property.
1262
+ d : numpy.ndarray
1263
+ Values of the property.
1264
+ notation : dict, optional
1265
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
1266
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
1267
+
1268
+ Attributes
1269
+ ----------
1270
+ _name : str
1271
+ Name of the property.
1272
+ _d : numpy.ndarray
1273
+ Values of the property.
1274
+ _notation : tilupy.notations.Notation
1275
+ Instance of the class :class:`tilupy.notations.Notation`.
1276
+ """
1277
+ def __init__(self, name: str, d: np.ndarray, notation: dict=None):
1278
+ super().__init__(name, d, notation=notation)
1279
+ # x and y arrays
1280
+
1281
+
1282
+ @abstractmethod
1283
+ def plot(self):
1284
+ """Abstract method to plot the results."""
1285
+ pass
1286
+
1287
+
1288
+ @abstractmethod
1289
+ def save(self):
1290
+ """Abstract method to save the results."""
1291
+ pass
1292
+
1293
+
1294
+ class StaticResults0D(StaticResults):
1295
+ """
1296
+ Class for simulation results described where the data is one or multiple scalar.
1297
+
1298
+ Parameters
1299
+ ----------
1300
+ name : str
1301
+ Name of the property.
1302
+ d : numpy.ndarray
1303
+ Values of the property.
1304
+ notation : dict, optional
1305
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
1306
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
1307
+
1308
+ Attributes
1309
+ ----------
1310
+ _name : str
1311
+ Name of the property.
1312
+ _d : numpy.ndarray
1313
+ Values of the property.
1314
+ _notation : tilupy.notations.Notation
1315
+ Instance of the class :class:`tilupy.notations.Notation`.
1316
+ """
1317
+ def __init__(self, name: str, d: np.ndarray, notation: dict=None):
1318
+ super().__init__(name, d, notation=notation)
1319
+
1320
+
1321
+ class StaticResults1D(StaticResults):
1322
+ """
1323
+ Class for simulation results described by one dimension for space.
1324
+
1325
+ Parameters
1326
+ ----------
1327
+ name : str
1328
+ Name of the property.
1329
+ d : numpy.ndarray
1330
+ Values of the property.
1331
+ coords: numpy.ndarray
1332
+ Spatial coordinates.
1333
+ coords_name: str
1334
+ Spatial coordinates name.
1335
+ notation : dict, optional
1336
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
1337
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
1338
+
1339
+ Attributes
1340
+ ----------
1341
+ _name : str
1342
+ Name of the property.
1343
+ _d : numpy.ndarray
1344
+ Values of the property.
1345
+ _notation : tilupy.notations.Notation
1346
+ Instance of the class :class:`tilupy.notations.Notation`.
1347
+ _coords: numpy.ndarray
1348
+ Spatial coordinates.
1349
+ _coords_name: str
1350
+ Spatial coordinates name.
1351
+ """
1352
+ def __init__(self,
1353
+ name: str,
1354
+ d: np.ndarray,
1355
+ coords: np.ndarray=None,
1356
+ coords_name: list[str]=None,
1357
+ notation: dict=None
1358
+ ):
1359
+ super().__init__(name, d, notation=notation)
1360
+ # x and y arrays
1361
+ self._coords = coords
1362
+ self._coords_name = coords_name
1363
+
1364
+
1365
+ def plot(self,
1366
+ ax: matplotlib.axes._axes.Axes = None,
1367
+ **kwargs
1368
+ ) -> matplotlib.axes._axes.Axes:
1369
+ """Plot the 1D results.
1370
+
1371
+ Parameters
1372
+ ----------
1373
+ ax : matplotlib.axes._axes.Axes, optional
1374
+ Existing matplotlib window, if None create one. By default None.
1375
+
1376
+ Returns
1377
+ -------
1378
+ matplotlib.axes._axes.Axes
1379
+ The created plot.
1380
+ """
1381
+ if ax is None:
1382
+ fig, ax = plt.subplots(1, 1, layout="constrained")
1383
+
1384
+ if isinstance(self._d, np.ndarray):
1385
+ data = self._d.T
1386
+ else:
1387
+ data = self._d
1388
+
1389
+ if "color" not in kwargs:
1390
+ color = "black"
1391
+ kwargs["color"] = color
1392
+
1393
+ ax.plot(self._coords, data, **kwargs)
1394
+
1395
+ ax.grid(True, alpha=0.3)
1396
+ ax.set_xlim(left=min(self._coords), right=max(self._coords))
1397
+ ax.set_xlabel(notations.get_label(self._coords_name))
1398
+ ax.set_ylabel(notations.get_label(self._notation))
1399
+
1400
+ return ax
1401
+
1402
+
1403
+ @property
1404
+ def coords(self) -> np.ndarray:
1405
+ """Get spatial coordinates.
1406
+
1407
+ Returns
1408
+ -------
1409
+ numpy.ndarray
1410
+ Attribute :attr:`_coords`
1411
+ """
1412
+ return self._coords
1413
+
1414
+
1415
+ @property
1416
+ def coords_name(self) -> str:
1417
+ """Get spatial coordinates name.
1418
+
1419
+ Returns
1420
+ -------
1421
+ str
1422
+ Attribute :attr:`_coords_name`
1423
+ """
1424
+ return self._coords_name
1425
+
1426
+
1427
+ class StaticResults2D(StaticResults):
1428
+ """
1429
+ Class for simulation results described by a two dimensional space result.
1430
+ Inherits from StaticResults.
1431
+
1432
+ Parameters
1433
+ ----------
1434
+ name : str
1435
+ Name of the property.
1436
+ d : numpy.ndarray
1437
+ Values of the property.
1438
+ x : numpy.ndarray
1439
+ X coordinate values.
1440
+ y : numpy.ndarray
1441
+ X coordinate values.
1442
+ z : numpy.ndarray
1443
+ Elevation values of the surface.
1444
+ notation : dict, optional
1445
+ Dictionnary of argument for creating an instance of the class :class:`tilupy.notations.Notation`.
1446
+ If None use the function :func:`tilupy.notations.get_notation`. By default None.
1447
+
1448
+ Attributes
1449
+ ----------
1450
+ _name : str
1451
+ Name of the property.
1452
+ _d : numpy.ndarray
1453
+ Values of the property.
1454
+ _notation : tilupy.notations.Notation
1455
+ Instance of the class :class:`tilupy.notations.Notation`.
1456
+ _x : numpy.ndarray
1457
+ X coordinate values.
1458
+ _y : numpy.ndarray
1459
+ X coordinate values.
1460
+ _z : numpy.ndarray
1461
+ Elevation values of the surface.
1462
+ """
1463
+ def __init__(self,
1464
+ name: str,
1465
+ d: np.ndarray,
1466
+ x: np.ndarray=None,
1467
+ y: np.ndarray=None,
1468
+ z: np.ndarray=None,
1469
+ notation: dict=None
1470
+ ):
1471
+ super().__init__(name, d, notation=notation)
1472
+ # x and y arrays
1473
+ self._x = x
1474
+ self._y = y
1475
+ # topography
1476
+ self._z = z
1477
+
1478
+
1479
+ def plot(self,
1480
+ figsize: tuple[float] = None,
1481
+ x: np.ndarray = None,
1482
+ y: np.ndarray = None,
1483
+ z: np.ndarray = None,
1484
+ sup_plt_fn: Callable = None,
1485
+ sup_plt_fn_args: dict = None,
1486
+ ax: matplotlib.axes._axes.Axes = None,
1487
+ **kwargs
1488
+ ) -> matplotlib.axes._axes.Axes:
1489
+ """Plot the 2D results using :func:`pytopomap.plot.plot_data_on_topo`.
1490
+
1491
+ Parameters
1492
+ ----------
1493
+ figsize : tuple[float], optional
1494
+ Size of the figure, by default None
1495
+ x : numpy.ndarray, optional
1496
+ X coordinate values, if None use :attr:`_x`. By default None.
1497
+ y : numpy.ndarray, optional
1498
+ Y coordinate values, if None use :attr:`_y`. By default None.
1499
+ z : numpy.ndarray, optional
1500
+ Elevation values, if None use :attr:`_z`. By default None.
1501
+ sup_plt_fn : callable, optional
1502
+ A custom function to apply additional plotting on the axes, by default None.
1503
+ sup_plt_fn_args : dict, optional
1504
+ Arguments to pass to :data:`sup_plt_fn`, by default None.
1505
+ ax : matplotlib.axes._axes.Axes, optional
1506
+ Existing matplotlib window, if None create one. By default None
1507
+ kwargs
1508
+ Additional arguments to pass to :func:`pytopomap.plot.plot_data_on_topo`.
1509
+
1510
+ Returns
1511
+ -------
1512
+ matplotlib.axes._axes.Axes
1513
+ The created plot.
1514
+
1515
+ Raises
1516
+ ------
1517
+ TypeError
1518
+ If no value for x, y.
1519
+ """
1520
+ if ax is None:
1521
+ fig, ax = plt.subplots(1, 1, figsize=figsize, layout="constrained")
1522
+
1523
+ if x is None:
1524
+ x = self._x
1525
+ if y is None:
1526
+ y = self._y
1527
+ if z is None:
1528
+ z = self._z
1529
+
1530
+ if x is None or y is None or z is None:
1531
+ raise TypeError("x, y or z data missing")
1532
+
1533
+ if "colorbar_kwargs" not in kwargs:
1534
+ kwargs["colorbar_kwargs"] = dict()
1535
+ if "label" not in kwargs["colorbar_kwargs"]:
1536
+ clabel = notations.get_label(self._notation)
1537
+ kwargs["colorbar_kwargs"]["label"] = clabel
1538
+
1539
+ ax = plt_fn.plot_data_on_topo(x,
1540
+ y,
1541
+ z,
1542
+ self._d,
1543
+ axe=ax,
1544
+ figsize=figsize,
1545
+ **kwargs)
1546
+ if sup_plt_fn is not None:
1547
+ if sup_plt_fn_args is None:
1548
+ sup_plt_fn_args = dict()
1549
+ sup_plt_fn(ax, **sup_plt_fn_args)
1550
+
1551
+ return ax
1552
+
1553
+
1554
+ def save(self,
1555
+ folder: str=None,
1556
+ file_name: str=None,
1557
+ fmt: str="txt",
1558
+ x: np.ndarray=None,
1559
+ y: np.ndarray=None,
1560
+ **kwargs
1561
+ ) -> None:
1562
+ """Save the 2D results.
1563
+
1564
+ Parameters
1565
+ ----------
1566
+ folder : str, optional
1567
+ Path to the output folder, if None create a folder with :attr:`_name`. By default None.
1568
+ file_name : str, optional
1569
+ Base name for the output image files, if None use :attr:`_name`. By default None.
1570
+ fmt : str, optional
1571
+ File format for saving result, by default "txt".
1572
+ x : np.ndarray, optional
1573
+ X coordinate values, if None use :attr:`_x`. By default None.
1574
+ y : np.ndarray, optional
1575
+ Y coordinate values, if None use :attr:`_y`. By default None.
1576
+
1577
+ Raises
1578
+ ------
1579
+ ValueError
1580
+ If no value for x, y.
1581
+ """
1582
+ if x is None:
1583
+ x = self._x
1584
+ if y is None:
1585
+ y = self._y
1586
+
1587
+ if x is None or y is None:
1588
+ raise ValueError("x et y arrays must not be None")
1589
+
1590
+ if file_name is None:
1591
+ file_name = self._name + "." + fmt
1592
+
1593
+ if folder is not None:
1594
+ file_name = os.path.join(folder, file_name)
1595
+
1596
+ tilupy.raster.write_raster(x, y, self._d, file_name, fmt=fmt, **kwargs)
1597
+
1598
+
1599
+ def get_spatial_stat(self,
1600
+ stat: str,
1601
+ axis=None
1602
+ ) -> tilupy.read.StaticResults0D | tilupy.read.StaticResults1D:
1603
+ """Statistical analysis along spatial dimension for 2D results.
1604
+
1605
+ Parameters
1606
+ ----------
1607
+ stat : str
1608
+ Statistical operator to apply. Must be implemented in :data:`NP_OPERATORS`.
1609
+ axis : tuple[int]
1610
+ Axis where to do the analysis:
1611
+
1612
+ - If axis is string, replace 'x' by 1, 'y' by 0 and 'xy' by (0, 1).
1613
+ - If axis is int, only use 0 or 1.
1614
+ - If None use (0, 1). By default None.
1615
+
1616
+ Returns
1617
+ -------
1618
+ tilupy.read.StaticResults0D or tilupy.read.StaticResults1D
1619
+ Instance of :class:`tilupy.read.StaticResults0D` or :class:`tilupy.read.StaticResults1D`.
1620
+
1621
+ """
1622
+ if axis is None:
1623
+ axis = (0, 1)
1624
+
1625
+ if isinstance(axis, str):
1626
+ axis_str = axis
1627
+ if axis == "x":
1628
+ axis = 1
1629
+ elif axis == "y":
1630
+ axis = 0
1631
+ elif axis == "xy":
1632
+ axis = (0, 1)
1633
+ else:
1634
+ if axis == 1:
1635
+ axis_str = "x"
1636
+ elif axis == 0:
1637
+ axis_str = "y"
1638
+ elif axis == (0, 1):
1639
+ axis_str = "xy"
1640
+
1641
+ if stat in NP_OPERATORS:
1642
+ dnew = getattr(np, stat)(self._d, axis=axis)
1643
+ elif stat == "int":
1644
+ dnew = np.sum(self._d, axis=axis)
1645
+ if axis == 1:
1646
+ dd = self._x[1] - self._x[0]
1647
+ elif axis == 0:
1648
+ dd = self._y[1] - self._y[0]
1649
+ elif axis == (0, 1):
1650
+ dd = (self._x[1] - self._x[0]) * (self._y[1] - self._y[0])
1651
+ dnew = dnew * dd
1652
+
1653
+ if axis == 1:
1654
+ # Needed to get correct orinetation as d[0, 0] is the upper corner
1655
+ # of the data, with coordinates x[0], y[-1]
1656
+ dnew = np.flip(dnew, axis=0)
1657
+
1658
+ new_name = self._name + "_" + stat + "_" + axis_str
1659
+ notation = notations.add_operator(self._notation, stat, axis=axis_str)
1660
+
1661
+ if axis == (0, 1):
1662
+ return StaticResults0D(new_name,
1663
+ dnew,
1664
+ notation=notation)
1665
+ else:
1666
+ if axis == 0:
1667
+ coords = self._x
1668
+ coords_name = "x"
1669
+ else:
1670
+ coords = self._y
1671
+ coords_name = "y"
1672
+ return StaticResults1D(new_name,
1673
+ dnew,
1674
+ coords,
1675
+ coords_name=coords_name,
1676
+ notation=notation,)
1677
+
1678
+
1679
+ @property
1680
+ def x(self) -> np.ndarray:
1681
+ """Get X coordinates.
1682
+
1683
+ Returns
1684
+ -------
1685
+ numpy.ndarray
1686
+ Attribute :attr:`_x`.
1687
+ """
1688
+ return self._x
1689
+
1690
+
1691
+ @property
1692
+ def y(self) -> np.ndarray:
1693
+ """Get Y coordinates.
1694
+
1695
+ Returns
1696
+ -------
1697
+ numpy.ndarray
1698
+ Attribute :attr:`_y`.
1699
+ """
1700
+ return self._y
1701
+
1702
+
1703
+ @property
1704
+ def z(self) -> np.ndarray:
1705
+ """Get elevations values.
1706
+
1707
+ Returns
1708
+ -------
1709
+ numpy.ndarray
1710
+ Attribute :attr:`_z`.
1711
+ """
1712
+ return self._z
1713
+
1714
+
1715
+ class Results:
1716
+ """Results of thin-layer model simulation
1717
+
1718
+ This class is the parent class for all simulation results, whatever the
1719
+ kind of input data. Methods and functions for processing results are given
1720
+ here. Reading results from code specific outputs is done in inhereited
1721
+ classes.
1722
+
1723
+ This class has global attributes used by all child classes and quick access
1724
+ attributes calculated and stored for easier access to the main results of a
1725
+ simulation. The quick attributes are only computed if needed and can be deleted
1726
+ to clean memory.
1727
+
1728
+ Parameters
1729
+ ----------
1730
+ args and kwargs :
1731
+ Specific arguments for each models.
1732
+
1733
+ Attributes
1734
+ ----------
1735
+ _code : str
1736
+ Name of the code that generated the result.
1737
+ _folder : str
1738
+ Path to find code files (like parameters).
1739
+ _folder_output :
1740
+ Path to find the results of the code.
1741
+ _zinit : numpy.ndarray
1742
+ Surface elevation of the simulation.
1743
+ _tim : list
1744
+ Lists of recorded time steps.
1745
+ _x : numpy.ndarray
1746
+ X-coordinates of the simulation.
1747
+ _y : numpy.ndarray
1748
+ Y-coordinates of the simulation.
1749
+ _dx : float
1750
+ Cell size along X-coordinates.
1751
+ _dy : float
1752
+ Cell size along Y-coordinates.
1753
+ _nx : float
1754
+ Number of cells along X-coordinates.
1755
+ _ny : float
1756
+ Number of cells along Y-coordinates.
1757
+
1758
+ _h : tilupy.read.TemporalResults2D
1759
+ Quick access attributes for fluid height over time.
1760
+ _h_max : tilupy.read.TemporalResults0D
1761
+ Quick access attributes for max fluid hieght over time.
1762
+ _u : tilupy.read.TemporalResults2D
1763
+ Quick access attributes for norm of fluid velocity over time.
1764
+ _u_max : tilupy.read.TemporalResults0D
1765
+ Quick access attributes for max norm of fluid velocity over time.
1766
+ _costh : numpy.ndarray
1767
+ Quick access attributes for value of cos[theta] at any point on the surface.
1768
+ """
1769
+ def __init__(self, *args, **kwargs):
1770
+ self._h = None
1771
+ self._h_max = None
1772
+ self._u = None
1773
+ self._u_max = None
1774
+ self._costh = None
1775
+
1776
+ self._code = None
1777
+ self._folder = None
1778
+ self._folder_output = None
1779
+ self._z = None
1780
+ self._zinit = None
1781
+ self._tim = None
1782
+ self._x = None
1783
+ self._y = None
1784
+ self._dx = None
1785
+ self._dy = None
1786
+ self._nx = None
1787
+ self._ny = None
1788
+
1789
+
1790
+ def compute_costh(self) -> np.ndarray:
1791
+ """Get cos(slope) of topography.
1792
+
1793
+ Returns
1794
+ -------
1795
+ numpy.ndarray
1796
+ Value of cos[theta] at any point on the surface.
1797
+ """
1798
+ [Fx, Fy] = np.gradient(self._zinit, np.flip(self._y), self._x)
1799
+ costh = 1 / np.sqrt(1 + Fx**2 + Fy**2)
1800
+ return costh
1801
+
1802
+
1803
+ def center_of_mass(self, h_thresh: float=None) -> tilupy.read.TemporalResults0D:
1804
+ """Compute center of mass coordinates depending on time.
1805
+
1806
+ Parameters
1807
+ ----------
1808
+ h_thresh : float, optional
1809
+ Value of threshold for the flow height, by default None.
1810
+
1811
+ Returns
1812
+ -------
1813
+ tilupy.read.TemporalResults0D
1814
+ Values of center of mass coordinates.
1815
+ """
1816
+ dx = self._x[1] - self._x[0]
1817
+ dy = self._y[1] - self._y[0]
1818
+ # Make meshgrid
1819
+ X, Y = np.meshgrid(self._x, np.flip(self._y))
1820
+
1821
+ if self._h is None:
1822
+ self.h
1823
+
1824
+ # Weights for coordinates average (volume in cell / total volume)
1825
+ h2 = self._h.copy()
1826
+ if h_thresh is not None:
1827
+ h2[h2 < h_thresh] = 0
1828
+ if self._costh is None:
1829
+ self._costh = self.compute_costh()
1830
+ w = h2 / self._costh[:, :, np.newaxis] * dx * dy
1831
+ vol = np.nansum(w, axis=(0, 1))
1832
+ w = w / vol[np.newaxis, np.newaxis, :]
1833
+ # Compute center of mass coordinates
1834
+ nt = h2.shape[2]
1835
+ coord = np.zeros((3, nt))
1836
+ tmp = X[:, :, np.newaxis] * w
1837
+ coord[0, :] = np.nansum(tmp, axis=(0, 1))
1838
+ tmp = Y[:, :, np.newaxis] * w
1839
+ coord[1, :] = np.nansum(tmp, axis=(0, 1))
1840
+ tmp = self._zinit[:, :, np.newaxis] * w
1841
+ coord[2, :] = np.nansum(tmp, axis=(0, 1))
1842
+
1843
+ # Make TemporalResults
1844
+ res = TemporalResults0D("centermass",
1845
+ coord,
1846
+ self._tim,
1847
+ scalar_names=["X", "Y", "z"],
1848
+ notation=None)
1849
+ return res
1850
+
1851
+
1852
+ def volume(self, h_thresh: float=None) -> tilupy.read.TemporalResults0D:
1853
+ """Compute flow volume depending on time.
1854
+
1855
+ Parameters
1856
+ ----------
1857
+ h_thresh : float, optional
1858
+ Value of threshold for the flow height, by default None.
1859
+
1860
+ Returns
1861
+ -------
1862
+ tilupy.read.TemporalResults0D
1863
+ Values of flow volumes.
1864
+ """
1865
+ dx = self._x[1] - self._x[0]
1866
+ dy = self._y[1] - self._y[0]
1867
+
1868
+ if self._h is None:
1869
+ self._h = self.get_output("h").d
1870
+ h2 = self._h.copy()
1871
+ if h_thresh is not None:
1872
+ h2[h2 < h_thresh] = 0
1873
+ if self._costh is None:
1874
+ self._costh = self.compute_costh()
1875
+ w = h2 / self._costh[:, :, np.newaxis] * dx * dy
1876
+ vol = np.nansum(w, axis=(0, 1))
1877
+ res = TemporalResults0D("volume",
1878
+ vol,
1879
+ self._tim,
1880
+ notation=None)
1881
+ return res
1882
+
1883
+
1884
+ def get_output(self,
1885
+ output_name: str,
1886
+ from_file: bool=True,
1887
+ **kwargs
1888
+ ) -> tilupy.read.TemporalResults0D | tilupy.read.StaticResults2D | tilupy.read.TemporalResults2D:
1889
+ """Get all the available outputs for a simulation :
1890
+ - Topographic outputs : "z", "zinit", "costh"
1891
+ - Temporal 2D outputs : "hvert", "h", "u", "ux", "uy", "hu", "hu2"
1892
+ - Other outputs : "centermass", "volume"
1893
+
1894
+ It is possible to add operators to temporal 2D outputs :
1895
+ - "max", "mean", "std", "sum", "min", "final", "init", "int"
1896
+
1897
+ And it is possible to add axis (only if using operators) :
1898
+ - "x", "y", "xy"
1899
+
1900
+ Parameters
1901
+ ----------
1902
+ output_name : str
1903
+ Name of the wanted output, composed of the output name and potentially
1904
+ an operator and an axis: :data:`output_operator_axis`.
1905
+ from_file : bool, optional
1906
+ If True, find the output in a specific file. By default True.
1907
+
1908
+ Returns
1909
+ -------
1910
+ tilupy.read.TemporalResults0D, tilupy.read.StaticResults2D or tilupy.read.TemporalResults2D
1911
+ Wanted output.
1912
+ """
1913
+ # Specific case of center of mass
1914
+ if output_name == "centermass":
1915
+ return self.center_of_mass(**kwargs)
1916
+
1917
+ # Specific case of volume
1918
+ if output_name == "volume":
1919
+ return self.volume(**kwargs)
1920
+
1921
+ strs = output_name.split("_")
1922
+ n_strs = len(strs)
1923
+
1924
+ res = None
1925
+
1926
+ # get topography
1927
+ if output_name in TOPO_DATA_2D:
1928
+ if output_name == "z":
1929
+ output_name = "zinit"
1930
+ output_name = '_' + output_name
1931
+ res = StaticResults2D(output_name,
1932
+ getattr(self, output_name),
1933
+ x=self._x,
1934
+ y=self._y,
1935
+ z=self._z,
1936
+ notation=None)
1937
+ return res
1938
+
1939
+ # If no operator is called, call directly extract_output
1940
+ if n_strs == 1:
1941
+ res = self._extract_output(output_name, **kwargs)
1942
+ return res
1943
+
1944
+ # Otherwise, get name, operator and axis (optional)
1945
+ name = strs[0]
1946
+ operator = strs[1]
1947
+ axis = None
1948
+ if n_strs == 3 :
1949
+ axis = strs[2]
1950
+
1951
+ # If processed output is read directly from file, call the child method
1952
+ # read_from_file.
1953
+ if from_file:
1954
+ try:
1955
+ res = self._read_from_file(name, operator, axis=axis, **kwargs)
1956
+ if res is None:
1957
+ raise UserWarning(f"{output_name} not found with _read_from_file for {self._code}, use get_spatial_stat")
1958
+ elif isinstance(res, str):
1959
+ raise UserWarning(res)
1960
+ except UserWarning as w:
1961
+ print(f"[WARNING] {w}")
1962
+ res = None
1963
+ # res is None in case of function failure
1964
+
1965
+ # If no results could be read from file, output must be
1966
+ # processed by tilupy
1967
+ if res is None:
1968
+ # Get output from name
1969
+ res = self._extract_output(name, x=self._x, y=self._y, **kwargs)
1970
+ if axis is None:
1971
+ # If no axis is given, the operator operates over time by
1972
+ # default
1973
+ res = res.get_temporal_stat(operator)
1974
+ else:
1975
+ if axis == "t":
1976
+ res = res.get_temporal_stat(operator)
1977
+ else:
1978
+ res = res.get_spatial_stat(operator, axis=axis)
1979
+
1980
+ return res
1981
+
1982
+
1983
+ def clear_quick_results(self) -> None:
1984
+ """Clear memory by erasing quick access attributes: :attr:`_h`, :attr:`_h_max`, :attr:`_u`, :attr:`_u_max`, :attr:`_costh`.
1985
+ """
1986
+ self._h = None
1987
+ self._h_max = None
1988
+ self._u = None
1989
+ self._u_max = None
1990
+ self._costh = None
1991
+
1992
+
1993
+ def get_profile(self,
1994
+ output: str,
1995
+ extraction_method: str = "axis",
1996
+ **extraction_params
1997
+ ) -> tuple[tilupy.read.TemporalResults1D | tilupy.read.StaticResults1D, np.ndarray]:
1998
+ """Extract a profile from a 2D data.
1999
+
2000
+ Parameters
2001
+ ----------
2002
+ output : str
2003
+ Wanted data output.
2004
+ extraction_mode : str, optional
2005
+ Method to extract profiles:
2006
+
2007
+ - "axis": Extracts a profile along an axis.
2008
+ - "coordinates": Extracts a profile along specified coordinates.
2009
+ - "shapefile": Extracts a profile along a shapefile (polylines).
2010
+
2011
+ Be default "axis".
2012
+ extraction_params : dict, optional
2013
+ Different parameters to be entered depending on the extraction method chosen.
2014
+ See :meth:`tilupy.utils.get_profile`.
2015
+
2016
+ Returns
2017
+ -------
2018
+ tuple[tilupy.read.TemporalResults1D | tilupy.read.StaticResults1D, np.ndarray]
2019
+ profile : tilupy.read.TemporalResults1D | tilupy.read.StaticResults1D
2020
+ Extracted profile.
2021
+ data : numpy.ndarray or float or tuple[nympy.ndarray, nympy.ndarray]
2022
+ Specific output depending on :data:`extraction_mode`:
2023
+
2024
+ - If :data:`extraction_mode == "axis"`: float
2025
+ Position of the profile.
2026
+ - If :data:`extraction_mode == "coordinates"`: tuple[numpy.ndarray]
2027
+ X coordinates, Y coordinates and distance values.
2028
+ - If :data:`extraction_mode == "shapefile"`: numpy.ndarray
2029
+ Distance values.
2030
+
2031
+ Raises
2032
+ ------
2033
+ ValueError
2034
+ If :data:`output` doesn't generate a 2D data.
2035
+ """
2036
+ data = self.get_output(output)
2037
+
2038
+ if not isinstance(data, tilupy.read.TemporalResults2D) and not isinstance(data, tilupy.read.StaticResults2D):
2039
+ raise ValueError("Can only extract profile from 2D data.")
2040
+
2041
+ profile, data = utils.get_profile(data, extraction_method, **extraction_params)
2042
+
2043
+ return profile, data
2044
+
2045
+
2046
+ def plot(self,
2047
+ output: str,
2048
+ from_file: bool =True, #get_output
2049
+ h_thresh: float=None, #get_output
2050
+ time_steps: float | list[float] = None,
2051
+ save: bool = False,
2052
+ folder_out: str = None,
2053
+ dpi: int = 150,
2054
+ fmt: str="png",
2055
+ file_suffix: str = None,
2056
+ file_prefix: str = None,
2057
+ display_plot: bool = True,
2058
+ **plot_kwargs
2059
+ ) -> matplotlib.axes._axes.Axes:
2060
+ """Plot output extracted from model's result.
2061
+
2062
+ Parameters
2063
+ ----------
2064
+ output : str
2065
+ Wanted output to be plotted. Must be in :data:`DATA_NAMES`.
2066
+ from_file : bool, optional
2067
+ If True, find the output in a specific file. By default True.
2068
+ h_thresh : float, optional
2069
+ Threshold value to be taken into account when extracting output, by default None.
2070
+ time_steps : float or list[float], optional
2071
+ Time steps to show when plotting temporal data. If None shows every time
2072
+ steps recorded. By default None.
2073
+ save : bool, optional
2074
+ If True, save the plot as an image to the computer, by default False.
2075
+ folder_out : str, optional
2076
+ Path to the folder where to save the plot, by default None.
2077
+ dpi : int, optional
2078
+ Resolution for the saved plot, by default 150.
2079
+ fmt : str, optional
2080
+ Format of the saved plot, by default "png".
2081
+ file_suffix : str, optional
2082
+ Suffix to add to the file name when saving, by default None.
2083
+ file_prefix : str, optional
2084
+ Prefix to add to the file name when saving, by default None.
2085
+ display_plot : bool, optional
2086
+ If True, enables the display of the plot; otherwise, it disables the display to save memory.
2087
+ By default True.
2088
+
2089
+ Returns
2090
+ -------
2091
+ matplotlib.axes._axes.Axes
2092
+ Wanted plot.
2093
+ """
2094
+ if not display_plot:
2095
+ backend = plt.get_backend()
2096
+ plt.close("all")
2097
+ plt.switch_backend("Agg")
2098
+
2099
+ if output in ["z", "zinit", "z_init"]:
2100
+ topo_kwargs = dict()
2101
+ if "topo_kwargs" in plot_kwargs:
2102
+ topo_kwargs = plot_kwargs["topo_kwargs"]
2103
+ axe = plt_fn.plot_topo(self._zinit, self._x, self._y, **topo_kwargs)
2104
+ return axe
2105
+
2106
+ data = self.get_output(output, from_file=from_file, h_thresh=h_thresh)
2107
+
2108
+ add_time_on_plot = False
2109
+ if (isinstance(time_steps, float) or isinstance(time_steps, int)) and isinstance(data, tilupy.read.TemporalResults):
2110
+ t_index = np.argmin(np.abs(self._tim - time_steps))
2111
+ add_time_on_plot = self._tim[t_index]
2112
+
2113
+ if time_steps is not None and isinstance(data, tilupy.read.TemporalResults):
2114
+ data = data.extract_from_time_step(time_steps)
2115
+
2116
+ if save:
2117
+ if folder_out is None:
2118
+ assert (self._folder_output is not None), "folder_output attribute must be set"
2119
+ folder_out = os.path.join(self._folder_output, "plots")
2120
+ os.makedirs(folder_out, exist_ok=True)
2121
+
2122
+ # TODO
2123
+ # Edit Temporal/Static.plot() pour que la sauvegarde soit directement intégrer dans les méthodes plots
2124
+ # Dans les plots, modifier les fonctions pour appeler des méthodes de pytopomap selon le plot voulu
2125
+ # (shotgater, profil, surface, etc...) et créer une fonction globale qui appelle chaque sous méthode
2126
+ # pour créer les graphes et gérer la sauvegarde
2127
+ if folder_out is not None and isinstance(data, TemporalResults2D):
2128
+ # If data is TemporalResults2D then saving is managed directly
2129
+ # by the associated plot method
2130
+ plot_kwargs["folder_out"] = folder_out
2131
+ plot_kwargs["dpi"] = dpi
2132
+ plot_kwargs["fmt"] = fmt
2133
+ # kwargs["file_suffix"] = file_prefix
2134
+ # kwargs["file_prefix"] = file_prefix
2135
+
2136
+ axe = data.plot(**plot_kwargs)
2137
+
2138
+ if add_time_on_plot:
2139
+ axe.set_title(f"t={add_time_on_plot}s", loc="left")
2140
+
2141
+ if folder_out is not None and not isinstance(data, TemporalResults2D):
2142
+ file_name = output
2143
+ if file_suffix is not None:
2144
+ file_name = file_name + "_" + file_suffix
2145
+ if file_prefix is not None:
2146
+ file_name = file_prefix + "_" + file_name
2147
+ file_out = os.path.join(folder_out, file_name + "." + fmt)
2148
+ # axe.figure.tight_layout(pad=0.1)
2149
+ axe.figure.savefig(file_out, dpi=dpi, bbox_inches="tight", pad_inches=0.05)
2150
+
2151
+ if not display_plot:
2152
+ plt.close("all")
2153
+ plt.switch_backend(backend)
2154
+
2155
+ return axe
2156
+
2157
+
2158
+ def plot_profile(self,
2159
+ output: str,
2160
+ from_file: bool=True,
2161
+ extraction_method: str = "axis",
2162
+ extraction_params: dict = None,
2163
+ time_steps: float | list[float] = None,
2164
+ save: bool = False,
2165
+ folder_out: str = None,
2166
+ display_plot: bool = True,
2167
+ **plot_kwargs
2168
+ ) -> matplotlib.axes._axes.Axes:
2169
+ """Plot a 1D output extracted from a 2D output.
2170
+
2171
+ Parameters
2172
+ ----------
2173
+ output : str
2174
+ Wanted 2D output to extract the profile from. Must be in :data:`STATIC_DATA_2D`
2175
+ or in :data:`TEMPORAL_DATA_2D`.
2176
+ from_file : bool, optional
2177
+ If True, find the output in a specific file. By default True.
2178
+ extraction_mode : str, optional
2179
+ Method to extract profiles:
2180
+
2181
+ - "axis": Extracts a profile along an axis.
2182
+ - "coordinates": Extracts a profile along specified coordinates.
2183
+ - "shapefile": Extracts a profile along a shapefile (polylines).
2184
+ Be default "axis".
2185
+ extraction_params : dict, optional
2186
+ Different parameters to be entered depending on the extraction method chosen.
2187
+ See :meth:`tilupy.utils.get_profile`.
2188
+ time_steps : float or list[float], optional
2189
+ Time steps to show when plotting temporal data. If None shows every time
2190
+ steps recorded. By default None.
2191
+ save : bool, optional
2192
+ If True, save the plot as an image to the computer, by default False.
2193
+ folder_out : str, optional
2194
+ Path to the folder where to save the plot, by default None.
2195
+ display_plot : bool, optional
2196
+ If True, enables the display of the plot; otherwise, it disables the display to save memory.
2197
+ By default True.
2198
+
2199
+ Returns
2200
+ -------
2201
+ matplotlib.axes._axes.Axes
2202
+ Wanted plot.
2203
+
2204
+ Raises
2205
+ ------
2206
+ ValueError
2207
+ If the :data:`output` is not a 2D output.
2208
+ """
2209
+ if not display_plot:
2210
+ backend = plt.get_backend()
2211
+ plt.close("all")
2212
+ plt.switch_backend("Agg")
2213
+
2214
+ data = self.get_output(output, from_file=from_file)
2215
+
2216
+ if not isinstance(data, tilupy.read.TemporalResults2D) and not isinstance(data, tilupy.read.StaticResults2D):
2217
+ raise ValueError("Can only extract profile from 2D data.")
2218
+
2219
+ extraction_params = {} if extraction_params is None else extraction_params
2220
+
2221
+ profile, _ = utils.get_profile(data, extraction_method, **extraction_params)
2222
+ closest_value = False
2223
+
2224
+ if (isinstance(time_steps, float) or isinstance(time_steps, int)) and isinstance(data, tilupy.read.TemporalResults):
2225
+ t_index = np.argmin(np.abs(self._tim - time_steps))
2226
+ closest_value = self._tim[t_index]
2227
+
2228
+ if time_steps is not None and isinstance(data, tilupy.read.TemporalResults):
2229
+ profile = profile.extract_from_time_step(time_steps)
2230
+
2231
+ axe = profile.plot(**plot_kwargs)
2232
+
2233
+ if closest_value:
2234
+ axe.set_title(f"t={closest_value}s", loc="left")
2235
+
2236
+ if save:
2237
+ if folder_out is None:
2238
+ assert (self._folder_output is not None), "folder_output attribute must be set"
2239
+ folder_out = os.path.join(self._folder_output, "plots")
2240
+ os.makedirs(folder_out, exist_ok=True)
2241
+
2242
+ # TODO
2243
+ # Same as plot() -> add save mode in plot functions in Temporal/StaticResults
2244
+ '''
2245
+ if folder_out is not None and isinstance(data, TemporalResults2D):
2246
+ # If data is TemporalResults2D then saving is managed directly
2247
+ # by the associated plot method
2248
+ kwargs["folder_out"] = folder_out
2249
+ kwargs["dpi"] = dpi
2250
+ kwargs["fmt"] = fmt
2251
+ # kwargs["file_suffix"] = file_prefix
2252
+ # kwargs["file_prefix"] = file_prefix
2253
+
2254
+
2255
+
2256
+ if folder_out is not None and not isinstance(data, TemporalResults2D):
2257
+ file_name = output_name
2258
+ if file_suffix is not None:
2259
+ file_name = file_name + "_" + file_suffix
2260
+ if file_prefix is not None:
2261
+ file_name = file_prefix + "_" + file_name
2262
+ file_out = os.path.join(folder_out, file_name + "." + fmt)
2263
+ # axe.figure.tight_layout(pad=0.1)
2264
+ axe.figure.savefig(file_out, dpi=dpi, bbox_inches="tight", pad_inches=0.05)
2265
+ '''
2266
+
2267
+ if not display_plot:
2268
+ plt.close("all")
2269
+ plt.switch_backend(backend)
2270
+
2271
+ return axe
2272
+
2273
+
2274
+ def save(self,
2275
+ output_name: str,
2276
+ folder_out: str=None,
2277
+ file_name: str=None,
2278
+ fmt: str="txt",
2279
+ from_file: bool=True,
2280
+ **kwargs
2281
+ ) -> None:
2282
+ """Save simulation outputs (processed results or topographic data) to disk.
2283
+
2284
+ Depending on the requested output_name, the method either:
2285
+
2286
+ - Retrieves a result via :meth:`get_output` and calls its own :meth:`save` method,
2287
+ - Or, for static topography data, writes it directly to a raster file.
2288
+
2289
+ Parameters
2290
+ ----------
2291
+ output_name : str
2292
+ Name of the variable or processed result to save
2293
+ (e.g., "h", "u_mean_t", "centermass", or topographic data like "zinit").
2294
+ folder_out : str, optional
2295
+ Destination folder for saving files. If None, defaults to
2296
+ :data:`_folder_output/processed`. By default None.
2297
+ file_name : str, optional
2298
+ Base name of the output file (without extension). If None, uses
2299
+ :data:`output_name`. By default None.
2300
+ fmt : str, optional
2301
+ Output file format (e.g., "txt", "npy", "asc"), by default "txt".
2302
+ from_file : bool, optional
2303
+ If True, attempt to read precomputed results from file before computing,
2304
+ by default True.
2305
+ **kwargs : dict
2306
+ Extra arguments passed to the underlying save function. For raster data,
2307
+ forwarded to :func:`tilupy.raster.write_raster`.
2308
+
2309
+ Raises
2310
+ ------
2311
+ AssertionError
2312
+ If neither :data:`folder_out` nor :attr:`_folder_output` is defined.
2313
+ """
2314
+
2315
+ if folder_out is None:
2316
+ assert (self._folder_output is not None), "folder_output attribute must be set"
2317
+ folder_out = os.path.join(self._folder_output, "processed")
2318
+ os.makedirs(folder_out, exist_ok=True)
2319
+
2320
+ if output_name in DATA_NAMES:
2321
+ data = self.get_output(output_name, from_file=from_file)
2322
+ if data.d.ndim > 1:
2323
+ if "x" not in kwargs:
2324
+ kwargs["x"] = self.x
2325
+ if "y" not in kwargs:
2326
+ kwargs["y"] = self.y
2327
+
2328
+ data.save(folder=folder_out, file_name=file_name, fmt=fmt, **kwargs)
2329
+
2330
+ elif output_name in TOPO_DATA_2D:
2331
+ if file_name is None:
2332
+ file_name = output_name
2333
+ name = "_" + output_name
2334
+ file_out = os.path.join(folder_out, file_name)
2335
+ tilupy.raster.write_raster(self._x,
2336
+ self._y,
2337
+ getattr(self, name),
2338
+ file_out,
2339
+ fmt=fmt,
2340
+ **kwargs)
2341
+
2342
+
2343
+ @abstractmethod
2344
+ def _extract_output(self):
2345
+ """Abstract method to extract output of simulation result files."""
2346
+ pass
2347
+
2348
+
2349
+ @abstractmethod
2350
+ def _read_from_file(self):
2351
+ """Abstract method for reading output from specific files."""
2352
+ pass
2353
+
2354
+
2355
+ @property
2356
+ def zinit(self):
2357
+ """Get initial topography.
2358
+
2359
+ Returns
2360
+ -------
2361
+ numpy.ndarray
2362
+ Attribute :attr:`_zinit`
2363
+ """
2364
+ return self._zinit
2365
+
2366
+
2367
+ @property
2368
+ def z(self):
2369
+ """Get initial topography, alias for zinit.
2370
+
2371
+ Returns
2372
+ -------
2373
+ numpy.ndarray
2374
+ Attribute :attr:`_zinit`
2375
+ """
2376
+ return self._zinit
2377
+
2378
+
2379
+ @property
2380
+ def x(self):
2381
+ """Get X-coordinates.
2382
+
2383
+ Returns
2384
+ -------
2385
+ numpy.ndarray
2386
+ Attribute :attr:`_x`
2387
+ """
2388
+ return self._x
2389
+
2390
+
2391
+ @property
2392
+ def y(self):
2393
+ """Get Y-coordinates.
2394
+
2395
+ Returns
2396
+ -------
2397
+ numpy.ndarray
2398
+ Attribute :attr:`_y`
2399
+ """
2400
+ return self._y
2401
+
2402
+
2403
+ @property
2404
+ def dx(self):
2405
+ """Get cell size along X.
2406
+
2407
+ Returns
2408
+ -------
2409
+ numpy.ndarray
2410
+ Attribute :attr:`_dx`
2411
+ """
2412
+ return self._dx
2413
+
2414
+
2415
+ @property
2416
+ def dy(self):
2417
+ """Get cell size along Y.
2418
+
2419
+ Returns
2420
+ -------
2421
+ numpy.ndarray
2422
+ Attribute :attr:`_dy`
2423
+ """
2424
+ return self._dy
2425
+
2426
+
2427
+ @property
2428
+ def nx(self):
2429
+ """Get number of cells along X.
2430
+
2431
+ Returns
2432
+ -------
2433
+ numpy.ndarray
2434
+ Attribute :attr:`_nx`
2435
+ """
2436
+ return self._nx
2437
+
2438
+
2439
+ @property
2440
+ def ny(self):
2441
+ """Get number of cells along Y.
2442
+
2443
+ Returns
2444
+ -------
2445
+ numpy.ndarray
2446
+ Attribute :attr:`_ny`
2447
+ """
2448
+ return self._ny
2449
+
2450
+
2451
+ @property
2452
+ def tim(self):
2453
+ """Get recorded time steps.
2454
+
2455
+ Returns
2456
+ -------
2457
+ numpy.ndarray
2458
+ Attribute :attr:`_tim`
2459
+ """
2460
+ return self._tim
2461
+
2462
+
2463
+ @property
2464
+ def h(self):
2465
+ """Get flow thickness. Compute it if not stored.
2466
+
2467
+ Returns
2468
+ -------
2469
+ tilupy.read.TemporalResults2D
2470
+ Attribute :attr:`_h`
2471
+ """
2472
+ if self._h is None:
2473
+ self._h = self.get_output("h").d
2474
+ return self._h
2475
+
2476
+
2477
+ @property
2478
+ def h_max(self):
2479
+ """Get maximum flow thickness. Compute it if not stored.
2480
+
2481
+ Returns
2482
+ -------
2483
+ tilupy.read.TemporalResults0D
2484
+ Attribute :attr:`_h_max`
2485
+ """
2486
+ if self._h_max is None:
2487
+ self._h_max = self.get_output("h_max").d
2488
+ return self._h_max
2489
+
2490
+
2491
+ @property
2492
+ def u(self):
2493
+ """Get flow velocity. Compute it if not stored.
2494
+
2495
+ Returns
2496
+ -------
2497
+ tilupy.read.TemporalResults2D
2498
+ Attribute :attr:`_u`
2499
+ """
2500
+ if self._u is None:
2501
+ self._u = self.get_output("u").d
2502
+ return self._u
2503
+
2504
+
2505
+ @property
2506
+ def u_max(self):
2507
+ """Get maximum flow velocity. Compute it if not stored.
2508
+
2509
+ Returns
2510
+ -------
2511
+ tilupy.read.TemporalResults0D
2512
+ Attribute :attr:`_u_max`
2513
+ """
2514
+ if self._u_max is None:
2515
+ self._u_max = self.get_output("u_max").d
2516
+ return self._u_max
2517
+
2518
+
2519
+ @property
2520
+ def costh(self):
2521
+ """Get cos(slope) of topography. Compute it if not stored.
2522
+
2523
+ Returns
2524
+ -------
2525
+ numpy.ndarray
2526
+ Attribute :attr:`_costh`
2527
+ """
2528
+ if self._costh is None:
2529
+ self._costh = self.compute_costh()
2530
+ return self._costh
2531
+
2532
+
2533
+ def get_results(code, **kwargs) -> tilupy.read.Results:
2534
+ """Get simulation results for a given numerical model.
2535
+
2536
+ Dynamically imports the corresponding reader module from
2537
+ `tilupy.models.<code>.read` and instantiates its :class:`tilupy.read.Results` class.
2538
+
2539
+ Parameters
2540
+ ----------
2541
+ code : str
2542
+ Short name of the simulation model: must be in :data:`ALLOWED_MODELS`.
2543
+ **kwargs : dict
2544
+ Additional keyword arguments passed to the :class:`tilupy.read.Results` constructor
2545
+ of the imported module.
2546
+
2547
+ Returns
2548
+ -------
2549
+ tilupy.read.Results
2550
+ Instance of the :class:`tilupy.read.Results` class containing the simulation outputs.
2551
+
2552
+ Raises
2553
+ ------
2554
+ ModuleNotFoundError
2555
+ If the module `tilupy.models.<code>.read` cannot be imported.
2556
+ AttributeError
2557
+ If the module does not define a :class:`tilupy.read.Results` class.
2558
+ """
2559
+ module = importlib.import_module("tilupy.models." + code + ".read")
2560
+ return module.Results(**kwargs)
2561
+
2562
+
2563
+ def use_thickness_threshold(simu: tilupy.read.Results,
2564
+ array: np.ndarray,
2565
+ h_thresh: float
2566
+ ) -> np.ndarray:
2567
+ """Apply a flow thickness threshold to mask simulation results.
2568
+
2569
+ Values of :data:`array` are set to zero wherever the flow thickness
2570
+ is below the given threshold.
2571
+
2572
+ Parameters
2573
+ ----------
2574
+ simu : tilupy.read.Results
2575
+ Simulation result object providing access to thickness data :data:`h`.
2576
+ array : numpy.ndarray
2577
+ Array of values to be masked (must be consistent in shape with thickness).
2578
+ h_thresh : float
2579
+ Thickness threshold. Cells with thickness < :data:`h_thresh` are set to zero.
2580
+
2581
+ Returns
2582
+ -------
2583
+ numpy.ndarray
2584
+ Thresholded array, with values set to zero where flow thickness is too low.
2585
+ """
2586
+ thickness = simu.get_output("h")
2587
+ array[thickness.d < h_thresh] = 0
2588
+ return array