• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ghiggi / gpm_api / 8501239008

31 Mar 2024 10:12PM UTC coverage: 87.857% (+0.2%) from 87.669%
8501239008

Pull #53

github

ghiggi
Add flake8-comprehensions
Pull Request #53: Refactor code style

701 of 796 new or added lines in 86 files covered. (88.07%)

4 existing lines in 4 files now uncovered.

9001 of 10245 relevant lines covered (87.86%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/gpm/visualization/plot_3d.py
1
# -----------------------------------------------------------------------------.
2
# MIT License
3

4
# Copyright (c) 2024 GPM-API developers
5
#
6
# This file is part of GPM-API.
7

8
# Permission is hereby granted, free of charge, to any person obtaining a copy
9
# of this software and associated documentation files (the "Software"), to deal
10
# in the Software without restriction, including without limitation the rights
11
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
# copies of the Software, and to permit persons to whom the Software is
13
# furnished to do so, subject to the following conditions:
14
#
15
# The above copyright notice and this permission notice shall be included in all
16
# copies or substantial portions of the Software.
17
#
18
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
# SOFTWARE.
25

26
# -----------------------------------------------------------------------------.
27
"""This module contains functions for 3D visualization of GPM-API RADAR data."""
×
28
import numpy as np
×
29

30
# TODO:
31
# - Isosurface contour buggy at low reflectivity
32
#   --> Should I replace values as 0-1 at each round?
33
# - 3D terrain
34
# - surface on bin surface height
35

36

37
def create_pyvista_2d_surface(data_array, spacing=(1, 1, 1), origin=(0, 0, 0)):
×
38
    """Create pyvista ImageData object from 2D xr.DataArray."""
39
    import pyvista as pv
×
40

41
    dimensions = (data_array.shape[0], data_array.shape[1], 1)
×
42
    surf = pv.ImageData(
×
43
        dimensions=dimensions,
44
        spacing=spacing,
45
        origin=origin,
46
    )
NEW
47
    data = data_array.to_numpy()
×
48
    data = data[:, ::-1]
×
49
    surf.point_data.set_array(data.flatten(order="F"), name=data_array.name)
×
50
    surf.set_active_scalars(data_array.name)
×
51
    return surf
×
52

53

54
def create_pyvista_3d_volume(data_array, spacing=(1, 1, 0.25), origin=(0, 0, 0)):
×
55
    """Create pyvista ImageData object from 3D xr.DataArray."""
56
    import pyvista as pv
×
57

58
    # Remove vertical areas without values
59
    data_array = data_array.gpm.slice_range_with_valid_data()
×
60

61
    # Create ImageData object
62
    # - TODO: scale (factor)
63
    dimensions = data_array.shape
×
64
    vol = pv.ImageData(
×
65
        dimensions=dimensions,
66
        spacing=spacing,
67
        origin=origin,
68
    )
NEW
69
    data = data_array.to_numpy()
×
70
    data = data[:, ::-1, ::-1]
×
71
    vol.point_data.set_array(data.flatten(order="F"), name=data_array.name)
×
72
    vol.set_active_scalars(data_array.name)
×
73
    return vol
×
74

75

76
def get_slider_button_positions(i, num_buttons, spacing_factor=0.04):
×
77
    """Return the pointa and pointb parameters for pl.add_slider_widget."""
78
    if num_buttons < 1:
×
79
        raise ValueError("Number of buttons must be at least 1")
×
80

81
    # Define margin
82
    min_pointa = 0.025
×
83
    max_pointb = 0.98
×
84

85
    # Define allowable buttons width
86
    total_width = max_pointb - min_pointa - spacing_factor * (num_buttons - 1)
×
87

88
    # Define button width
89
    button_width = total_width / num_buttons
×
90

91
    # Define pointa and pointb
92
    start_x = min_pointa + i * (button_width)
×
93
    if i > 0:
×
94
        start_x = start_x + spacing_factor
×
95
    end_x = start_x + button_width
×
96
    pointa = (start_x, 0.1)
×
97
    pointb = (end_x, 0.1)
×
98
    return pointa, pointb
×
99

100

101
class OpacitySlider:
×
102
    """Opacity Slider for pyvista pl.add_slider_widget."""
103

104
    def __init__(self, pl_actor):
×
105
        self.pl_actor = pl_actor
×
106

107
    def __call__(self, value):
×
108
        self.pl_actor.GetProperty().SetOpacity(value)
×
109

110

111
def add_3d_isosurfaces(
×
112
    vol,
113
    pl,
114
    isovalues=[30, 40, 50],
115
    opacities=[0.3, 0.5, 1],
116
    method="contour",
117
    style="surface",
118
    add_sliders=False,
119
    **mesh_kwargs,
120
):
121
    """Add 3D isosurface to a pyvista plotter object.
122

123
    If add_sliders=True, isosurface opacity can be adjusted interactively.
124

125
    """
126
    # Checks
127
    if len(isovalues) != len(opacities):
×
128
        raise ValueError("Expected same number of isovalues and opacities values.")
×
129
    # TODO: check there are values larger than max isovalues
130

131
    # Define opacity dictionary
132
    dict_opacity = dict(zip(isovalues, opacities))
×
133

134
    # Precompute isosurface
135
    dict_isosurface = {isovalue: vol.contour([isovalue], method=method) for isovalue in isovalues}
×
136
    n_isosurfaces = len(dict_isosurface)
×
137
    # Add isosurfaces
138
    for i, (isovalue, isosurface) in enumerate(dict_isosurface.items()):
×
139
        pl_actor = pl.add_mesh(
×
140
            isosurface,
141
            opacity=dict_opacity[isovalue],
142
            style=style,
143
            **mesh_kwargs,
144
        )
145
        if add_sliders:
×
146
            # Define opacity slider
147
            opacity_slider = OpacitySlider(pl_actor)
×
148
            # Define slicer button position
149
            pointa, pointb = get_slider_button_positions(i=i, num_buttons=n_isosurfaces)
×
150
            # Add slider widget
151
            pl.add_slider_widget(
×
152
                callback=opacity_slider,
153
                rng=[0, 1],
154
                value=dict_opacity[isovalue],
155
                title=f"Isovalue={isovalue}",
156
                pointa=pointa,
157
                pointb=pointb,
158
                style="modern",
159
            )
160

161

162
class IsosurfaceSlider:
×
163
    def __init__(self, vol, method="contour", isovalue=None):
×
164
        """Define pyvista slider for 3D isosurfaces."""
165

166
        self.vol = vol
×
167
        vmin, vmax = vol.get_data_range()
×
168
        self.vmin = vmin
×
169
        self.vmax = vmax
×
170
        self.method = method
×
171
        if isovalue is None:
×
172
            isovalue = vmin + (vmax - vmin) / 2
×
173
        self.isovalue = isovalue
×
174
        self.isosurface = vol.contour([isovalue], method=method)
×
175

176
    def __call__(self, value):
×
177
        self.isovalue = value
×
178
        self.update()
×
179

180
    def update(self):
×
181
        result = self.vol.contour([self.isovalue], method=self.method)
×
182
        self.isosurface.copy_from(result)
×
183

184

185
def add_3d_isosurface_slider(vol, pl, method="contour", isovalue=None, **mesh_kwargs):
×
186
    """Add a 3D isosurface slider enabling to slide through the 3D volume."""
187
    isosurface_slider = IsosurfaceSlider(vol, method=method, isovalue=isovalue)
×
188
    isosurface = isosurface_slider.isosurface
×
189
    pl.add_mesh(isosurface, **mesh_kwargs)
×
190
    pl.add_slider_widget(
×
191
        callback=isosurface_slider,
192
        rng=vol.get_data_range(),
193
        value=isosurface_slider.isovalue,
194
        title="Isosurface",
195
        pointa=(0.4, 0.9),
196
        pointb=(0.9, 0.9),
197
        style="modern",
198
    )
199

200

201
class OrthogonalSlicesSlider:
×
202
    def __init__(self, vol, x=1, y=1, z=1):
×
203
        """Define pyvista sliders for 3D orthogonal slices."""
204

205
        self.vol = vol
×
206
        self.slices = vol.slice_orthogonal(x=x, y=y, z=z)
×
207
        # Set default parameters
208
        self.kwargs = {
×
209
            "x": x,
210
            "y": y,
211
            "z": z,
212
        }
213

214
    def __call__(self, param, value):
×
215
        self.kwargs[param] = value
×
216
        self.update()
×
217

218
    def update(self):
×
219
        # This is where you call your simulation
220
        result = self.vol.slice_orthogonal(**self.kwargs)
×
221
        self.slices[0].copy_from(result[0])
×
222
        self.slices[1].copy_from(result[1])
×
223
        self.slices[2].copy_from(result[2])
×
224

225

226
def add_3d_orthogonal_slices(vol, pl, x=None, y=None, z=None, add_sliders=False, **mesh_kwargs):
×
227
    """Add 3D orthogonal slices with interactive sliders."""
228
    # Define bounds
229
    x_rng = vol.bounds[0:2]
×
230
    y_rng = vol.bounds[2:4]
×
231
    z_rng = vol.bounds[4:6]
×
232

233
    # Define default values if not provided
234
    # - If value is 0, means no slice plotted !
235
    if x is None:
×
236
        x = int(np.diff(x_rng) / 2)
×
237
    if y is None:
×
238
        y = int(np.diff(y_rng) / 2)
×
239
    if z is None:
×
240
        z = int(z_rng[0] + 0.01)
×
241

242
    # Define orthogonal slices (and sliders)
243
    if add_sliders:
×
244
        orthogonal_slices_slider = OrthogonalSlicesSlider(vol)
×
245
        orthogonal_slices = orthogonal_slices_slider.slices
×
246
    else:
247
        orthogonal_slices = vol.slice_orthogonal(x=x, y=y, z=z, progress_bar=False)
×
248

249
    # Display orthogonal slices
250
    pl.add_mesh(orthogonal_slices, **mesh_kwargs)
×
251

252
    # Add slider widgets
253
    if add_sliders:
×
254
        pl.add_slider_widget(
×
255
            callback=lambda value: orthogonal_slices_slider("x", int(value)),
256
            rng=x_rng,
257
            value=x,
258
            title="Along-Track",
259
            pointa=(0.025, 0.1),
260
            pointb=(0.31, 0.1),
261
            style="modern",
262
        )
263
        pl.add_slider_widget(
×
264
            callback=lambda value: orthogonal_slices_slider("y", int(value)),
265
            rng=y_rng,
266
            value=y,
267
            title="Cross-Track",
268
            pointa=(0.35, 0.1),
269
            pointb=(0.64, 0.1),
270
            style="modern",
271
        )
272
        pl.add_slider_widget(
×
273
            callback=lambda value: orthogonal_slices_slider("z", value),
274
            rng=z_rng,
275
            value=z,
276
            title="Elevation",
277
            pointa=(0.67, 0.1),
278
            pointb=(0.98, 0.1),
279
            style="modern",
280
        )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc