capytaine 2.3.1__cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.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.
Files changed (93) hide show
  1. capytaine/__about__.py +16 -0
  2. capytaine/__init__.py +36 -0
  3. capytaine/bem/__init__.py +0 -0
  4. capytaine/bem/airy_waves.py +111 -0
  5. capytaine/bem/engines.py +441 -0
  6. capytaine/bem/problems_and_results.py +600 -0
  7. capytaine/bem/solver.py +594 -0
  8. capytaine/bodies/__init__.py +4 -0
  9. capytaine/bodies/bodies.py +1221 -0
  10. capytaine/bodies/dofs.py +19 -0
  11. capytaine/bodies/predefined/__init__.py +6 -0
  12. capytaine/bodies/predefined/cylinders.py +151 -0
  13. capytaine/bodies/predefined/rectangles.py +111 -0
  14. capytaine/bodies/predefined/spheres.py +70 -0
  15. capytaine/green_functions/FinGreen3D/.gitignore +1 -0
  16. capytaine/green_functions/FinGreen3D/FinGreen3D.f90 +3589 -0
  17. capytaine/green_functions/FinGreen3D/LICENSE +165 -0
  18. capytaine/green_functions/FinGreen3D/Makefile +16 -0
  19. capytaine/green_functions/FinGreen3D/README.md +24 -0
  20. capytaine/green_functions/FinGreen3D/test_program.f90 +39 -0
  21. capytaine/green_functions/LiangWuNoblesse/.gitignore +1 -0
  22. capytaine/green_functions/LiangWuNoblesse/LICENSE +504 -0
  23. capytaine/green_functions/LiangWuNoblesse/LiangWuNoblesseWaveTerm.f90 +751 -0
  24. capytaine/green_functions/LiangWuNoblesse/Makefile +16 -0
  25. capytaine/green_functions/LiangWuNoblesse/README.md +2 -0
  26. capytaine/green_functions/LiangWuNoblesse/test_program.f90 +28 -0
  27. capytaine/green_functions/__init__.py +2 -0
  28. capytaine/green_functions/abstract_green_function.py +64 -0
  29. capytaine/green_functions/delhommeau.py +507 -0
  30. capytaine/green_functions/hams.py +204 -0
  31. capytaine/green_functions/libs/Delhommeau_float32.cpython-314t-x86_64-linux-gnu.so +0 -0
  32. capytaine/green_functions/libs/Delhommeau_float64.cpython-314t-x86_64-linux-gnu.so +0 -0
  33. capytaine/green_functions/libs/__init__.py +0 -0
  34. capytaine/io/__init__.py +0 -0
  35. capytaine/io/bemio.py +153 -0
  36. capytaine/io/legacy.py +328 -0
  37. capytaine/io/mesh_loaders.py +1086 -0
  38. capytaine/io/mesh_writers.py +692 -0
  39. capytaine/io/meshio.py +38 -0
  40. capytaine/io/wamit.py +479 -0
  41. capytaine/io/xarray.py +668 -0
  42. capytaine/matrices/__init__.py +16 -0
  43. capytaine/matrices/block.py +592 -0
  44. capytaine/matrices/block_toeplitz.py +325 -0
  45. capytaine/matrices/builders.py +89 -0
  46. capytaine/matrices/linear_solvers.py +232 -0
  47. capytaine/matrices/low_rank.py +395 -0
  48. capytaine/meshes/__init__.py +6 -0
  49. capytaine/meshes/clipper.py +465 -0
  50. capytaine/meshes/collections.py +342 -0
  51. capytaine/meshes/geometry.py +409 -0
  52. capytaine/meshes/mesh_like_protocol.py +37 -0
  53. capytaine/meshes/meshes.py +890 -0
  54. capytaine/meshes/predefined/__init__.py +6 -0
  55. capytaine/meshes/predefined/cylinders.py +314 -0
  56. capytaine/meshes/predefined/rectangles.py +261 -0
  57. capytaine/meshes/predefined/spheres.py +62 -0
  58. capytaine/meshes/properties.py +276 -0
  59. capytaine/meshes/quadratures.py +80 -0
  60. capytaine/meshes/quality.py +448 -0
  61. capytaine/meshes/surface_integrals.py +63 -0
  62. capytaine/meshes/symmetric.py +462 -0
  63. capytaine/post_pro/__init__.py +6 -0
  64. capytaine/post_pro/free_surfaces.py +88 -0
  65. capytaine/post_pro/impedance.py +92 -0
  66. capytaine/post_pro/kochin.py +54 -0
  67. capytaine/post_pro/rao.py +60 -0
  68. capytaine/tools/__init__.py +0 -0
  69. capytaine/tools/cache_on_disk.py +26 -0
  70. capytaine/tools/deprecation_handling.py +18 -0
  71. capytaine/tools/lists_of_points.py +52 -0
  72. capytaine/tools/lru_cache.py +49 -0
  73. capytaine/tools/optional_imports.py +27 -0
  74. capytaine/tools/prony_decomposition.py +150 -0
  75. capytaine/tools/symbolic_multiplication.py +149 -0
  76. capytaine/tools/timer.py +66 -0
  77. capytaine/ui/__init__.py +0 -0
  78. capytaine/ui/cli.py +28 -0
  79. capytaine/ui/rich.py +5 -0
  80. capytaine/ui/vtk/__init__.py +3 -0
  81. capytaine/ui/vtk/animation.py +329 -0
  82. capytaine/ui/vtk/body_viewer.py +28 -0
  83. capytaine/ui/vtk/helpers.py +82 -0
  84. capytaine/ui/vtk/mesh_viewer.py +461 -0
  85. capytaine-2.3.1.dist-info/LICENSE +674 -0
  86. capytaine-2.3.1.dist-info/METADATA +750 -0
  87. capytaine-2.3.1.dist-info/RECORD +93 -0
  88. capytaine-2.3.1.dist-info/WHEEL +6 -0
  89. capytaine-2.3.1.dist-info/entry_points.txt +3 -0
  90. capytaine.libs/libgfortran-83c28eba.so.5.0.0 +0 -0
  91. capytaine.libs/libgomp-e985bcbb.so.1.0.0 +0 -0
  92. capytaine.libs/libmvec-2-583a17db.28.so +0 -0
  93. capytaine.libs/libquadmath-2284e583.so.0.0.0 +0 -0
@@ -0,0 +1,204 @@
1
+ from importlib import import_module
2
+ from scipy.optimize import brentq
3
+ import numpy as np
4
+
5
+ from capytaine.green_functions.abstract_green_function import AbstractGreenFunction, GreenFunctionEvaluationError
6
+
7
+
8
+ class LiangWuNoblesseGF(AbstractGreenFunction):
9
+ """Wrapper for the Fortran implementation of the infinite depth Green function of [Liang, Wu, Noblesse, 2018].
10
+
11
+ Uses the same implementation as Delhommeau() for the Rankine and reflected Rankine terms.
12
+
13
+ """
14
+ floating_point_precision = "float64"
15
+
16
+ fortran_core = import_module("capytaine.green_functions.libs.Delhommeau_float64")
17
+ tabulation_grid_shape_index = fortran_core.constants.liang_wu_noblesse
18
+ exportable_settings = {'green_function': "LiangWuNoblesseGF"}
19
+
20
+ # Dummy arrays that won't actually be used by the fortran code.
21
+ prony_decomposition = np.zeros((1, 1))
22
+ dispersion_relation_roots = np.empty(1)
23
+ finite_depth_method_index = -9999
24
+ tabulation_nb_integration_points = 1
25
+ tabulated_r_range = np.empty(1)
26
+ tabulated_z_range = np.empty(1)
27
+ tabulated_integrals = np.empty(1)
28
+ dummy_param = -999
29
+
30
+ def __str__(self):
31
+ return "LiangWuNoblesseGF()"
32
+
33
+ def __repr__(self):
34
+ return "LiangWuNoblesseGF()"
35
+
36
+ def _repr_pretty_(self, p, cycle):
37
+ p.text(self.__repr__())
38
+
39
+ def evaluate(self,
40
+ mesh1, mesh2,
41
+ free_surface=0.0, water_depth=np.inf, wavenumber=1.0,
42
+ adjoint_double_layer=True, early_dot_product=True
43
+ ):
44
+
45
+ if free_surface == np.inf or water_depth < np.inf:
46
+ raise NotImplementedError("LiangWuNoblesseGF() is only implemented for infinite depth with a free surface")
47
+
48
+ if wavenumber == np.inf:
49
+ gf_singularities_index = self.fortran_core.constants.high_freq
50
+ else:
51
+ gf_singularities_index = self.fortran_core.constants.low_freq
52
+
53
+ collocation_points, early_dot_product_normals = \
54
+ self._get_colocation_points_and_normals(mesh1, mesh2, adjoint_double_layer)
55
+
56
+ S, K = self._init_matrices(
57
+ (collocation_points.shape[0], mesh2.nb_faces), early_dot_product=early_dot_product
58
+ )
59
+
60
+ self.fortran_core.matrices.build_matrices(
61
+ collocation_points, early_dot_product_normals,
62
+ mesh2.vertices, mesh2.faces + 1,
63
+ mesh2.faces_centers, mesh2.faces_normals,
64
+ mesh2.faces_areas, mesh2.faces_radiuses,
65
+ *mesh2.quadrature_points,
66
+ wavenumber, np.inf,
67
+ self.tabulation_nb_integration_points, self.tabulation_grid_shape_index,
68
+ self.tabulated_r_range, self.tabulated_z_range, self.tabulated_integrals,
69
+ self.dummy_param, self.prony_decomposition, self.dispersion_relation_roots,
70
+ gf_singularities_index, adjoint_double_layer,
71
+ S, K
72
+ )
73
+
74
+ if mesh1 is mesh2:
75
+ self.fortran_core.matrices.add_diagonal_term(
76
+ mesh2.faces_centers, early_dot_product_normals, free_surface, K,
77
+ )
78
+
79
+ if np.any(np.isnan(S)) or np.any(np.isnan(K)):
80
+ raise GreenFunctionEvaluationError(
81
+ "Green function returned a NaN in the interaction matrix.\n"
82
+ "It could be due to overlapping panels.")
83
+
84
+ if early_dot_product:
85
+ K = K.reshape((collocation_points.shape[0], mesh2.nb_faces))
86
+
87
+ return S, K
88
+
89
+
90
+ class FinGreen3D(AbstractGreenFunction):
91
+ """Wrapper for the Fortran implementation of the finite depth Green function of [Liu et al.].
92
+
93
+ Uses the same implementation as Delhommeau() for the Rankine and reflected Rankine terms.
94
+
95
+ """
96
+ floating_point_precision = "float64"
97
+
98
+ fortran_core = import_module("capytaine.green_functions.libs.Delhommeau_float64")
99
+ finite_depth_method_index = fortran_core.constants.fingreen3d_method
100
+ gf_singularities_index = fortran_core.constants.low_freq
101
+
102
+ # Dummy arrays that won't actually be used by the fortran code.
103
+ prony_decomposition = np.zeros((1, 1))
104
+ tabulation_nb_integration_points = 1
105
+ tabulated_r_range = np.empty(1)
106
+ tabulated_z_range = np.empty(1)
107
+ tabulated_integrals = np.empty(1)
108
+ dummy_param = -999
109
+
110
+ def __init__(self, *, nb_dispersion_roots=200):
111
+ self.nb_dispersion_roots = nb_dispersion_roots
112
+ self.exportable_settings = {
113
+ 'green_function': "FinGreen3D",
114
+ 'nb_dispersion_roots': nb_dispersion_roots
115
+ }
116
+
117
+ def __str__(self):
118
+ return f"FinGreen3D(nb_dispersion_roots={self.nb_dispersion_roots})"
119
+
120
+ def __repr__(self):
121
+ return f"FinGreen3D(nb_dispersion_roots={self.nb_dispersion_roots})"
122
+
123
+ def _repr_pretty_(self, p, cycle):
124
+ p.text(self.__repr__())
125
+
126
+ def compute_dispersion_relation_roots(self, nk, wavenumber, depth):
127
+ omega2_h_over_g = wavenumber*np.tanh(wavenumber*depth)*depth
128
+ def root(i_root):
129
+ return brentq(lambda y: omega2_h_over_g + y*np.tan(y), (2*i_root+1)*np.pi/2 + 1e-10, (2*i_root+2)*np.pi/2 - 1e-10)/depth
130
+ return np.array([wavenumber] + [root(i_root) for i_root in range(nk-1)])
131
+
132
+ def evaluate(self, mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer=True, early_dot_product=True):
133
+
134
+ if free_surface == np.inf or water_depth == np.inf:
135
+ raise NotImplementedError("FinGreen3D is only implemented for finite depth with a free surface.")
136
+ if wavenumber == 0.0 or wavenumber == np.inf:
137
+ raise NotImplementedError("FinGreen3D is only implemented for non-zero and non-infinite frequencies")
138
+
139
+ dispersion_relation_roots = self.compute_dispersion_relation_roots(
140
+ self.nb_dispersion_roots,
141
+ wavenumber,
142
+ water_depth
143
+ )
144
+
145
+ collocation_points, early_dot_product_normals = \
146
+ self._get_colocation_points_and_normals(mesh1, mesh2, adjoint_double_layer)
147
+
148
+ S, K = self._init_matrices(
149
+ (collocation_points.shape[0], mesh2.nb_faces), early_dot_product=early_dot_product
150
+ )
151
+
152
+ self.fortran_core.matrices.build_matrices(
153
+ collocation_points, early_dot_product_normals,
154
+ mesh2.vertices, mesh2.faces + 1,
155
+ mesh2.faces_centers, mesh2.faces_normals,
156
+ mesh2.faces_areas, mesh2.faces_radiuses,
157
+ *mesh2.quadrature_points,
158
+ wavenumber, water_depth,
159
+ self.tabulation_nb_integration_points, self.dummy_param,
160
+ self.tabulated_r_range, self.tabulated_z_range, self.tabulated_integrals,
161
+ self.finite_depth_method_index, self.prony_decomposition, dispersion_relation_roots,
162
+ self.gf_singularities_index, adjoint_double_layer,
163
+ S, K
164
+ )
165
+
166
+ if mesh1 is mesh2:
167
+ self.fortran_core.matrices.add_diagonal_term(
168
+ mesh2.faces_centers, early_dot_product_normals, free_surface, K,
169
+ )
170
+
171
+ if np.any(np.isnan(S)) or np.any(np.isnan(K)):
172
+ raise GreenFunctionEvaluationError(
173
+ "Green function returned a NaN in the interaction matrix.\n"
174
+ "It could be due to overlapping panels.")
175
+
176
+ if early_dot_product:
177
+ K = K.reshape((collocation_points.shape[0], mesh2.nb_faces))
178
+
179
+ return S, K
180
+
181
+
182
+ class HAMS_GF(AbstractGreenFunction):
183
+ floating_point_precision = "float64"
184
+
185
+ exportable_settings = {'green_function': "HAMS_GF"}
186
+
187
+ def __init__(self):
188
+ self.infinite_depth_gf = LiangWuNoblesseGF()
189
+ self.finite_depth_gf = FinGreen3D(nb_dispersion_roots=200)
190
+
191
+ def __str__(self):
192
+ return "HAMS_GF()"
193
+
194
+ def __repr__(self):
195
+ return "HAMS_GF()"
196
+
197
+ def _repr_pretty_(self, p, cycle):
198
+ p.text(self.__repr__())
199
+
200
+ def evaluate(self, mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer=True, early_dot_product=True):
201
+ if water_depth == np.inf:
202
+ return self.infinite_depth_gf.evaluate(mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer, early_dot_product)
203
+ else:
204
+ return self.finite_depth_gf.evaluate(mesh1, mesh2, free_surface, water_depth, wavenumber, adjoint_double_layer, early_dot_product)
File without changes
File without changes
capytaine/io/bemio.py ADDED
@@ -0,0 +1,153 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from scipy.optimize import newton
6
+
7
+ LOG = logging.getLogger(__name__)
8
+
9
+ #######################
10
+ # Import from Bemio #
11
+ #######################
12
+
13
+ def dataframe_from_bemio(bemio_obj, wavenumber, wavelength):
14
+ """Transform a :class:`bemio.data_structures.bem.HydrodynamicData` into a
15
+ :class:`pandas.DataFrame`.
16
+
17
+ Parameters
18
+ ----------
19
+ bemio_obj: Bemio data_stuctures.bem.HydrodynamicData class
20
+ Loaded NEMOH, AQWA, or WAMIT data created using `bemio.io.nemoh.read`,
21
+ `bemio.io.aqwa.read`, or `bemio.io.wamit.read` functions, respectively.
22
+ wavenumber: bool
23
+ If True, the coordinate 'wavenumber' will be added to the output dataset.
24
+ wavelength: bool
25
+ If True, the coordinate 'wavelength' will be added to the output dataset.
26
+ """
27
+
28
+
29
+ dofs = np.array(['Surge', 'Sway', 'Heave', 'Roll', 'Pitch', 'Yaw'])
30
+ for i in range(bemio_obj.body[0].num_bodies):
31
+ difr_dict = []
32
+ rad_dict = []
33
+
34
+ rho = bemio_obj.body[0].rho
35
+ g = bemio_obj.body[0].g
36
+
37
+ if bemio_obj.body[i].water_depth == 'infinite':
38
+ bemio_obj.body[i].water_depth = np.inf
39
+
40
+ from_wamit = (bemio_obj.body[i].bem_code == 'WAMIT') # WAMIT coefficients need to be dimensionalized
41
+
42
+ for omega_idx, omega in enumerate(np.sort(bemio_obj.body[i].w)):
43
+
44
+ # DiffractionProblem variable equivalents
45
+ for dir_idx, dir in enumerate(bemio_obj.body[i].wave_dir):
46
+ temp_dict = {}
47
+ temp_dict['body_name'] = bemio_obj.body[i].name
48
+ temp_dict['water_depth'] = bemio_obj.body[i].water_depth
49
+ temp_dict['omega'] = omega
50
+ temp_dict['freq'] = omega/(2*np.pi)
51
+ temp_dict['period'] = 2*np.pi/omega
52
+ temp_dict['rho'] = rho
53
+ temp_dict['g'] = g
54
+ temp_dict['kind'] = "DiffractionResult"
55
+ temp_dict['forward_speed'] = 0.0
56
+ temp_dict['free_surface'] = 0.0
57
+ temp_dict['wave_direction'] = np.radians(dir)
58
+ temp_dict['influenced_dof'] = dofs
59
+
60
+ if wavenumber or wavelength:
61
+ if temp_dict['water_depth'] == np.inf or omega**2*temp_dict['water_depth']/temp_dict['g'] > 20:
62
+ k = omega**2/temp_dict['g']
63
+ else:
64
+ k = newton(lambda x: x*np.tanh(x) - omega**2*temp_dict['water_depth']/temp_dict['g'], x0=1.0)/temp_dict['water_depth']
65
+
66
+ if wavenumber:
67
+ temp_dict['wavenumber'] = k
68
+
69
+ if wavelength:
70
+ if k == 0.0:
71
+ temp_dict['wavelength'] = np.inf
72
+ else:
73
+ temp_dict['wavelength'] = 2*np.pi/k
74
+
75
+ Fexc = np.empty(shape=bemio_obj.body[i].ex.re[:, dir_idx, omega_idx].shape, dtype=np.complex128)
76
+ if from_wamit:
77
+ Fexc.real = bemio_obj.body[i].ex.re[:, dir_idx, omega_idx] * rho * g
78
+ Fexc.imag = bemio_obj.body[i].ex.im[:, dir_idx, omega_idx] * rho * g
79
+ else:
80
+ Fexc.real = bemio_obj.body[i].ex.re[:, dir_idx, omega_idx]
81
+ Fexc.imag = bemio_obj.body[i].ex.im[:, dir_idx, omega_idx]
82
+ temp_dict['diffraction_force'] = Fexc.flatten()
83
+
84
+ try:
85
+ Fexc_fk = np.empty(shape=bemio_obj.body[i].ex.fk.re[:, dir_idx, omega_idx].shape, dtype=np.complex128)
86
+ if from_wamit:
87
+ Fexc_fk.real = bemio_obj.body[i].ex.fk.re[:, dir_idx, omega_idx] * rho * g
88
+ Fexc_fk.imag = bemio_obj.body[i].ex.fk.im[:, dir_idx, omega_idx] * rho * g
89
+ else:
90
+ Fexc_fk.real = bemio_obj.body[i].ex.fk.re[:, dir_idx, omega_idx]
91
+ Fexc_fk.imag = bemio_obj.body[i].ex.fk.im[:, dir_idx, omega_idx]
92
+ temp_dict['Froude_Krylov_force'] = Fexc_fk.flatten()
93
+
94
+ except AttributeError:
95
+ # LOG.warning('\tNo Froude-Krylov forces found for ' + bemio_obj.body[i].name + ' at ' + str(dir) + \
96
+ # ' degrees (omega = ' + str(omega) + '), replacing with zeros.')
97
+ temp_dict['Froude_Krylov_force'] = np.zeros((bemio_obj.body[i].ex.re[:, dir_idx, omega_idx].size,), dtype=np.complex128)
98
+
99
+ difr_dict.append(temp_dict)
100
+
101
+ # RadiationProblem + Hydrostatics variable equivalents
102
+ for radiating_dof_idx, radiating_dof in enumerate(dofs):
103
+ temp_dict = {}
104
+ temp_dict['body_name'] = bemio_obj.body[i].name
105
+ temp_dict['water_depth'] = bemio_obj.body[i].water_depth
106
+ temp_dict['omega'] = omega
107
+ temp_dict['freq'] = omega/(2*np.pi)
108
+ temp_dict['rho'] = rho
109
+ temp_dict['g'] = g
110
+ temp_dict['kind'] = "RadiationResult"
111
+ temp_dict['free_surface'] = 0.0
112
+ temp_dict['forward_speed'] = 0.0
113
+ temp_dict['wave_direction'] = 0.0
114
+ temp_dict['influenced_dof'] = dofs
115
+ temp_dict['radiating_dof'] = radiating_dof
116
+ temp_dict['added_mass'] = bemio_obj.body[i].am.all[radiating_dof_idx, :, omega_idx].flatten()
117
+ temp_dict['radiation_damping'] = bemio_obj.body[i].rd.all[radiating_dof_idx, :, omega_idx].flatten()
118
+
119
+ if from_wamit:
120
+ temp_dict['added_mass'] = temp_dict['added_mass'] * rho
121
+ temp_dict['radiation_damping'] = temp_dict['radiation_damping'] * rho * omega
122
+
123
+ if wavenumber or wavelength:
124
+ if temp_dict['water_depth'] == np.inf or omega**2*temp_dict['water_depth']/temp_dict['g'] > 20:
125
+ k = omega**2/temp_dict['g']
126
+ else:
127
+ k = newton(lambda x: x*np.tanh(x) - omega**2*temp_dict['water_depth']/temp_dict['g'], x0=1.0)/temp_dict['water_depth']
128
+
129
+ if wavenumber:
130
+ temp_dict['wavenumber'] = k
131
+
132
+ if wavelength:
133
+ if k == 0.0:
134
+ temp_dict['wavelength'] = np.inf
135
+ else:
136
+ temp_dict['wavelength'] = 2*np.pi/k
137
+
138
+ rad_dict.append(temp_dict)
139
+
140
+ df = pd.concat([
141
+ pd.DataFrame.from_dict(difr_dict).explode(['influenced_dof', 'diffraction_force', 'Froude_Krylov_force']),
142
+ pd.DataFrame.from_dict(rad_dict).explode(['influenced_dof', 'added_mass', 'radiation_damping'])
143
+ ])
144
+ df = df.astype({'added_mass': np.float64, 'radiation_damping': np.float64, 'diffraction_force': np.complex128, 'Froude_Krylov_force': np.complex128})
145
+
146
+ all_dofs_in_order = ['Surge', 'Sway', 'Heave', 'Roll', 'Pitch', 'Yaw']
147
+ inf_dof_cat = pd.CategoricalDtype(categories=all_dofs_in_order)
148
+ df["influenced_dof"] = df["influenced_dof"].astype(inf_dof_cat)
149
+ if 'added_mass' in df.columns:
150
+ rad_dof_cat = pd.CategoricalDtype(categories=all_dofs_in_order)
151
+ df["radiating_dof"] = df["radiating_dof"].astype(rad_dof_cat)
152
+
153
+ return df