From 65ca09374ad3d372ba1e6ffdea0ae47fa29dd6d2 Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Thu, 9 Dec 2021 11:56:23 +0900 Subject: [PATCH 01/51] :sparkles: Add fft and rfft filters --- examples/00-load/read-image.py | 17 +++++++++++++++++ tests/test_grid.py | 11 +++++++++++ 2 files changed, 28 insertions(+) diff --git a/examples/00-load/read-image.py b/examples/00-load/read-image.py index b0bb3bec07..6db37f80b6 100644 --- a/examples/00-load/read-image.py +++ b/examples/00-load/read-image.py @@ -32,3 +32,20 @@ # Mapped image colors image.plot(cpos="xy") + +############################################################################### +# It is also possible to apply filters to images. + +fft = image.fft() +rfft = image.rfft() +pl = pv.Plotter(1, 3) +pl.subplot(0, 0) +pl.add_title("Original") +pl.add_mesh(image) +pl.subplot(0, 1) +pl.add_title("FFT") +pl.add_mesh(fft) +pl.subplot(0, 2) +pl.add_title("rFFT") +pl.add_mesh(rfft) +pl.show(cpos="xy") diff --git a/tests/test_grid.py b/tests/test_grid.py index 444840b712..ae88ed6516 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -721,6 +721,17 @@ def test_cast_uniform_to_rectilinear(): assert rectilinear.bounds == grid.bounds +def test_fft_and_rfft(): + grid = examples.download_puppy() + fft = grid.fft() + assert fft.n_points == grid.n_points + assert fft.n_arrays == grid.n_arrays + rfft = fft.rfft() + assert rfft.n_points == grid.n_points + assert rfft.n_arrays == grid.n_arrays + assert np.allclose(rfft['JPEGImage'].real, grid['JPEGImage']) + + @pytest.mark.parametrize('binary', [True, False]) @pytest.mark.parametrize('extension', ['.vtk', '.vtr']) def test_save_rectilinear(extension, binary, tmpdir): From d3da172f0b858a1c13bee52b47a3f8ae7cfdedcc Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Sun, 19 Dec 2021 00:52:20 +0900 Subject: [PATCH 02/51] :sparkles: Add image_fft and image_rfft filters --- examples/00-load/read-image.py | 40 +++++++++++++-------- pyvista/_vtk.py | 4 +++ pyvista/core/filters/uniform_grid.py | 54 ++++++++++++++++++++++++++++ tests/test_grid.py | 14 ++++++-- 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/examples/00-load/read-image.py b/examples/00-load/read-image.py index 6db37f80b6..86813c4cba 100644 --- a/examples/00-load/read-image.py +++ b/examples/00-load/read-image.py @@ -5,6 +5,7 @@ Read and plot image files (JPEG, TIFF, PNG, etc). """ +import pyvista as pv from pyvista import examples ############################################################################### @@ -34,18 +35,27 @@ image.plot(cpos="xy") ############################################################################### -# It is also possible to apply filters to images. - -fft = image.fft() -rfft = image.rfft() -pl = pv.Plotter(1, 3) -pl.subplot(0, 0) -pl.add_title("Original") -pl.add_mesh(image) -pl.subplot(0, 1) -pl.add_title("FFT") -pl.add_mesh(fft) -pl.subplot(0, 2) -pl.add_title("rFFT") -pl.add_mesh(rfft) -pl.show(cpos="xy") +# Convert rgb to grayscale. +# https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems + +r = image["JPEGImage"][:, 0] +g = image["JPEGImage"][:, 1] +b = image["JPEGImage"][:, 2] +image.clear_data() +image["GrayScale"] = 0.299 * r + 0.587 * g + 0.114 * b +pv.global_theme.cmap = "gray" +image.copy().plot(cpos="xy") + +############################################################################### +# It is also possible to apply filters to images. The following is the Fast +# Fourier Transformed image data. + +fft = image.image_fft() +fft.copy().plot(cpos="xy", log_scale=True) + +############################################################################### +# Once Fast Fourier Transformed, images can also Reverse Fast Fourier +# Transformed. + +rfft = fft.image_rfft() +rfft.copy().plot(cpos="xy") diff --git a/pyvista/_vtk.py b/pyvista/_vtk.py index ea7cc7226f..e89fd17ed2 100644 --- a/pyvista/_vtk.py +++ b/pyvista/_vtk.py @@ -386,6 +386,10 @@ vtkOpenGLGPUVolumeRayCastMapper, vtkSmartVolumeMapper, ) + from vtkmodules.vtkImagingFourier import ( + vtkImageFFT, + vtkImageRFFT, + ) # lazy import for some of the less used readers def lazy_vtkGL2PSExporter(): diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 80fff1ed03..ea365e958c 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -116,3 +116,57 @@ def extract_subset(self, voi, rate=(1, 1, 1), boundary=False, progress_bar=False fixed.field_data.update(result.field_data) fixed.copy_meta_from(result) return fixed + + def image_fft(self, progress_bar=False): + """Fast Fourier Transform. + + The input can have real or complex data in any components and data types, + but the output is always complex doubles with real values in component0, + and imaginary values in component1. The filter is fastest for images + that have power of two sizes. The filter uses a butterfly diagram for + each prime factor of the dimension. This makes images with prime number + dimensions (i.e. 17x17) much slower to compute. Multi dimensional + (i.e volumes) FFT's are decomposed so that each axis executes serially. + + Parameters + ---------- + progress_bar : bool, optional + Display a progress bar to indicate progress. + + Returns + ------- + pyvista.UniformGrid + UniformGrid subset. + """ + alg = _vtk.vtkImageFFT() + alg.SetInputDataObject(self) + _update_alg(alg, progress_bar, 'Fast Fourier Transform.') + result = _get_output(alg) + return result + + def image_rfft(self, progress_bar=False): + """Reverse Fast Fourier Transform. + + The input can have real or complex data in any components and data types, + but the output is always complex doubles with real values in component0, + and imaginary values in component1. The filter is fastest for images that + have power of two sizes. The filter uses a butterfly diagram for each prime + factor of the dimension. This makes images with prime number dimensions + (i.e. 17x17) much slower to compute. Multi dimensional (i.e volumes) + FFT's are decomposed so that each axis executes serially. + + Parameters + ---------- + progress_bar : bool, optional + Display a progress bar to indicate progress. + + Returns + ------- + pyvista.UniformGrid + UniformGrid subset. + """ + alg = _vtk.vtkImageRFFT() + alg.SetInputDataObject(self) + _update_alg(alg, progress_bar, 'Reverse Fast Fourier Transform.') + result = _get_output(alg) + return result diff --git a/tests/test_grid.py b/tests/test_grid.py index ae88ed6516..d383accb3b 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -723,13 +723,21 @@ def test_cast_uniform_to_rectilinear(): def test_fft_and_rfft(): grid = examples.download_puppy() - fft = grid.fft() + # Convert rgb to grayscale. + # https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems + r = grid['JPEGImage'][:, 0] + g = grid['JPEGImage'][:, 1] + b = grid['JPEGImage'][:, 2] + grid.clear_data() + grid['GrayScale'] = 0.299 * r + 0.587 * g + 0.114 * b + fft = grid.image_fft() assert fft.n_points == grid.n_points assert fft.n_arrays == grid.n_arrays - rfft = fft.rfft() + rfft = fft.image_rfft() assert rfft.n_points == grid.n_points assert rfft.n_arrays == grid.n_arrays - assert np.allclose(rfft['JPEGImage'].real, grid['JPEGImage']) + assert np.allclose(rfft['GrayScale'][:, 0], grid['GrayScale']) + assert np.allclose(rfft['GrayScale'][:, 1], 0.0) @pytest.mark.parametrize('binary', [True, False]) From 4541538c1264ed7b1cbadefeaeac7bbedf44f7d6 Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Sun, 19 Dec 2021 15:16:47 +0900 Subject: [PATCH 03/51] Fix isort error --- pyvista/core/filters/unstructured_grid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyvista/core/filters/unstructured_grid.py b/pyvista/core/filters/unstructured_grid.py index 00d1d0b5dc..f5a6a4fab0 100644 --- a/pyvista/core/filters/unstructured_grid.py +++ b/pyvista/core/filters/unstructured_grid.py @@ -1,10 +1,9 @@ """Filters module with a class to manage filters/algorithms for unstructured grid datasets.""" from functools import wraps -import pyvista from pyvista import abstract_class -from pyvista.core.filters.poly_data import PolyDataFilters from pyvista.core.filters.data_set import DataSetFilters +from pyvista.core.filters.poly_data import PolyDataFilters @abstract_class From 3ea7491d6211453344c3963fc084dfbd107d9dce Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Fri, 25 Feb 2022 22:45:34 +0900 Subject: [PATCH 04/51] Update pyvista/core/filters/uniform_grid.py --- pyvista/core/filters/uniform_grid.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index cdfabac91d..7c600f7af7 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -309,6 +309,11 @@ def image_rfft(self, progress_bar=False): progress_bar : bool, optional Display a progress bar to indicate progress. UniformGrid subset. + + Returns + ------- + pyvista.UniformGrid + UniformGrid subset. """ alg = _vtk.vtkImageRFFT() alg.SetInputDataObject(self) From ed7a8ece083772811e8f296b0958f7f1ff49948b Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Fri, 1 Apr 2022 00:54:06 +0900 Subject: [PATCH 05/51] Fix isort --- pyvista/_vtk.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyvista/_vtk.py b/pyvista/_vtk.py index 177e4b9aa4..335c898110 100644 --- a/pyvista/_vtk.py +++ b/pyvista/_vtk.py @@ -384,6 +384,7 @@ except ImportError: # pragma: no cover pass + from vtkmodules.vtkImagingFourier import vtkImageFFT, vtkImageRFFT from vtkmodules.vtkRenderingCore import ( vtkActor, vtkActor2D, @@ -439,10 +440,6 @@ vtkSmartVolumeMapper, ) from vtkmodules.vtkViewsContext2D import vtkContextInteractorStyle - from vtkmodules.vtkImagingFourier import ( - vtkImageFFT, - vtkImageRFFT, - ) # lazy import for some of the less used readers def lazy_vtkGL2PSExporter(): From baf04d3e1c828b217700f055936af72fb6e62ae8 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 2 Jun 2022 22:56:58 -0600 Subject: [PATCH 06/51] improve examples --- examples/00-load/read-image.py | 27 --- examples/01-filter/image-fft-perlin-noise.py | 92 ++++++++ examples/01-filter/image-fft.py | 94 ++++++++ pyvista/_vtk.py | 7 +- pyvista/core/filters/uniform_grid.py | 214 ++++++++++++++++--- pyvista/examples/downloads.py | 30 +++ tests/conftest.py | 7 + tests/test_grid.py | 52 +++-- 8 files changed, 451 insertions(+), 72 deletions(-) create mode 100644 examples/01-filter/image-fft-perlin-noise.py create mode 100644 examples/01-filter/image-fft.py diff --git a/examples/00-load/read-image.py b/examples/00-load/read-image.py index 86813c4cba..b0bb3bec07 100644 --- a/examples/00-load/read-image.py +++ b/examples/00-load/read-image.py @@ -5,7 +5,6 @@ Read and plot image files (JPEG, TIFF, PNG, etc). """ -import pyvista as pv from pyvista import examples ############################################################################### @@ -33,29 +32,3 @@ # Mapped image colors image.plot(cpos="xy") - -############################################################################### -# Convert rgb to grayscale. -# https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems - -r = image["JPEGImage"][:, 0] -g = image["JPEGImage"][:, 1] -b = image["JPEGImage"][:, 2] -image.clear_data() -image["GrayScale"] = 0.299 * r + 0.587 * g + 0.114 * b -pv.global_theme.cmap = "gray" -image.copy().plot(cpos="xy") - -############################################################################### -# It is also possible to apply filters to images. The following is the Fast -# Fourier Transformed image data. - -fft = image.image_fft() -fft.copy().plot(cpos="xy", log_scale=True) - -############################################################################### -# Once Fast Fourier Transformed, images can also Reverse Fast Fourier -# Transformed. - -rfft = fft.image_rfft() -rfft.copy().plot(cpos="xy") diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py new file mode 100644 index 0000000000..dbb4726a1c --- /dev/null +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -0,0 +1,92 @@ +""" +.. _image_fft_perlin_example: + +Fast Fourier Transform with Perlin Noise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example shows how to apply a Fast Fourier Transform (FFT) to a +:class:`pyvista.UniformGrid` using :func:`pyvista.UniformGridFilters.fft` +filter. + +Here, we demonstrate FFT usage by first generating perlin noise using +:func:`pyvista.sample_function() ` to +sample :func:`pyvista.perlin_noise `, +and then performing FFT of the sampled noise to show the frequency content of +that noise. + +""" + +import numpy as np + +############################################################################### +# Start by generating some `Perlin Noise `_ +# +# Note that we are generating it in a flat plane and using 10 Hz in the x +# direction and 5 Hz in the y direction. +# +import pyvista as pv + +freq = [10, 5, 0] +noise = pv.perlin_noise(1, freq, (0, 0, 0)) +xdim, ydim = (500, 500) +sampled = pv.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(xdim, ydim, 1)) + +# plot the sampled noise +sampled.plot(cpos='xy', show_scalar_bar=False) + +############################################################################### +# Next, perform a FFT of the noise and plot the frequency content. +# For the sake of simplicity, we will only plot the content in the first +# quadrant. +# +# Note the usage of :func:`numpy.fft.fftfreq` to get the frequencies. +# +sampled_fft = sampled.fft() +freq = np.fft.fftfreq(sampled.dimensions[0], sampled.spacing[0]) + +# only show the first quadrant +subset = sampled_fft.extract_subset((0, xdim // 4, 0, ydim // 4, 0, 0)) + +# shift the position of the uniform grid to match the frequency +subset.origin = (0, 0, 0) +spacing = np.diff(freq[:2])[0] +subset.spacing = (spacing, spacing, spacing) + + +############################################################################### +# Now, plot the noise in the frequency domain. Note how there is more high +# frequency content in the x direction and this matches the frequencies given +# to :func:`pyvista.perlin_noise `. + +pl = pv.Plotter() +pl.add_mesh(subset, cmap='gray', show_scalar_bar=False) +pl.camera_position = 'xy' +pl.show_bounds(xlabel='X Frequency', ylabel='Y Frequency') +pl.show() + +############################################################################### +# Low Pass Filter +# ~~~~~~~~~~~~~~~ +# For fun, let's perform a low pass filter on the frequency content and then +# convert it back into the "time" domain by immediately apply a reverse FFT. +# +# As expected, we only see low frequency noise. + +low_pass = sampled_fft.low_pass(0.5, 0.5, 0.5).rfft() +# remove the complex data +low_pass['ImageScalars'] = low_pass['ImageScalars'][:, 0] +low_pass.plot(cpos='xy', show_scalar_bar=False) + + +############################################################################### +# High Pass Filter +# ~~~~~~~~~~~~~~~~ +# This time, let's perform a high pass filter on the frequency content and then +# convert it back into the "time" domain by immediately apply a reverse FFT. +# +# As expected, we only see the high frequency noise content as the low +# frequency noise has been attenuated. + +high_pass_noise = sampled_fft.high_pass(5, 5, 5).rfft() +high_pass_noise['ImageScalars'] = high_pass_noise['ImageScalars'][:, 0] +high_pass_noise.plot(cpos='xy', show_scalar_bar=False) diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py new file mode 100644 index 0000000000..dc469de531 --- /dev/null +++ b/examples/01-filter/image-fft.py @@ -0,0 +1,94 @@ +""" +.. _image_fft_example: + +Fast Fourier Transform +~~~~~~~~~~~~~~~~~~~~~~ + +This example shows how to apply a Fast Fourier Transform (FFT) to a +:class:`pyvista.UniformGrid` using :func:`pyvista.UniformGridFilters.fft` +filter. + +Here, we demonstrate FFT usage by denoising an image, effectively removing any +"high frequency" content by effectively performing a `low pass filter +`_ + +This example was inspired by `Image denoising by FFT +`_. + +""" + +import pyvista as pv +from pyvista import examples + +############################################################################### +# Load the example image moon-landing image and plot it. + +image = examples.download_moonlanding_image() +print(image.point_data) + +# Create a theme that we can reuse when plotting the image +grey_theme = pv.themes.DocumentTheme() +grey_theme.cmap = 'gray' +grey_theme.show_scalar_bar = False +grey_theme.axes.show = False +image.plot(theme=grey_theme, cpos='xy', text='Unprocessed Moon Landing Image') + +############################################################################### +# Apply FFT to the image +# +# FFT will be applied to the active scalars, which is stored in ``'PNGImage'``. +# The output from the filter contains both real and imaginary components and is +# stored in the same array. + +fft_image = image.fft() +fft_image.point_data + +############################################################################### +# Plot the FFT of the image. Note that we are effectively viewing the +# "frequency" of the data in this image, where the four corners contain the low +# frequency content of the image, and the middle is the high frequency content +# of the image. +# +# .. note:: +# VTK internally creates a normalized array to plot both the real and +# imaginary values from the FFT filter. To avoid having this array included +# in the dataset, we use :func:`copy() ` to create a +# temporary copy that's plotted. + +fft_image.copy().plot( + cpos="xy", + theme=grey_theme, + log_scale=True, + text='Moon Landing Image FFT', +) + +############################################################################### +# Remove the noise from the fft_image +# +# Effectively, we want to remove high frequency (noisy) data from our image. +# First, let's reshape by the size of the image. Note that the image data is in +# real and imaginary axes. +# +# Next, perform a low pass filter by removing the middle 80% of the content of +# the image. Note that the high frequency content is in the middle of the array. + +per_keep = 0.10 + +width, height, _ = fft_image.dimensions +data = fft_image['PNGImage'].reshape(height, width, 2) # note: axes flipped +data[int(height * per_keep) : -int(height * per_keep)] = 0 +data[:, int(width * per_keep) : -int(width * per_keep)] = 0 + +fft_image.copy().plot( + cpos="xy", + theme=grey_theme, + log_scale=True, + text='Moon Landing Image FFT with Noise Removed ', +) + + +############################################################################### +# Finally, convert the image data back to the "image domain" and plot it. + +rfft = fft_image.rfft() +rfft.plot(cpos="xy", theme=grey_theme, text='Processed Moon Landing Image') diff --git a/pyvista/_vtk.py b/pyvista/_vtk.py index 4f5e49f579..09478d822c 100644 --- a/pyvista/_vtk.py +++ b/pyvista/_vtk.py @@ -397,7 +397,12 @@ except ImportError: # pragma: no cover pass - from vtkmodules.vtkImagingFourier import vtkImageFFT, vtkImageRFFT + from vtkmodules.vtkImagingFourier import ( + vtkImageButterworthHighPass, + vtkImageButterworthLowPass, + vtkImageFFT, + vtkImageRFFT, + ) from vtkmodules.vtkRenderingCore import ( vtkActor, vtkActor2D, diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 089ccca118..b35960af9e 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -415,16 +415,20 @@ def image_threshold( _update_alg(alg, progress_bar, 'Performing Image Thresholding') return _get_output(alg) - def image_fft(self, progress_bar=False): - """Fast Fourier Transform. + def fft(self, progress_bar=False): + """Apply the fast fourier transform to the active scalars. - The input can have real or complex data in any components and data types, - but the output is always complex doubles with real values in component0, - and imaginary values in component1. The filter is fastest for images - that have power of two sizes. The filter uses a butterfly diagram for - each prime factor of the dimension. This makes images with prime number - dimensions (i.e. 17x17) much slower to compute. Multi dimensional - (i.e volumes) FFT's are decomposed so that each axis executes serially. + The input can have real or complex data in any components and data + types, but the output is always complex doubles, with the first + component containing real values and the second component containing + imaginary values. The filter is fastest for images that have power of + two sizes. + + The filter is fastest for images that have power of two sizes. The + filter uses a butterfly diagram for each prime factor of the + dimension. This makes images with prime number dimensions (i.e. 17x17) + much slower to compute. Multi dimensional (i.e volumes) FFT's are + decomposed so that each axis executes serially. Parameters ---------- @@ -434,38 +438,194 @@ def image_fft(self, progress_bar=False): Returns ------- pyvista.UniformGrid - UniformGrid subset. + UniformGrid with applied FFT. + + Examples + -------- + Apply FFT to an example image. + + >>> from pyvista import examples + >>> image = examples.download_moonlanding_image() + >>> fft_image = image.fft() + >>> fft_image.point_data # doctest:+SKIP + pyvista DataSetAttributes + Association : POINT + Active Scalars : PNGImage + Active Vectors : None + Active Texture : None + Active Normals : None + Contains arrays : + PNGImage float64 (298620, 2) SCALARS + + See :ref:`image_fft_example` for a full example using this filter. + """ + # check for active scalars, otherwise risk of segfault + if self.point_data.active_scalars_name is None: + raise ValueError('FFT filter requires active point scalars') + alg = _vtk.vtkImageFFT() alg.SetInputDataObject(self) - _update_alg(alg, progress_bar, 'Fast Fourier Transform.') - result = _get_output(alg) - return result + _update_alg(alg, progress_bar, 'Performing Fast Fourier Transform') + return _get_output(alg) + + def rfft(self, progress_bar=False): + """Apply the reverse fast fourier transform to the active scalars. - def image_rfft(self, progress_bar=False): - """Reverse Fast Fourier Transform. + The input can have real or complex data in any components and data + types, but the output is always complex doubles, with the first + component containing real values and the second component containing + imaginary values. The filter is fastest for images that have power of + two sizes. - The input can have real or complex data in any components and data types, - but the output is always complex doubles with real values in component0, - and imaginary values in component1. The filter is fastest for images that - have power of two sizes. The filter uses a butterfly diagram for each prime - factor of the dimension. This makes images with prime number dimensions - (i.e. 17x17) much slower to compute. Multi dimensional (i.e volumes) - FFT's are decomposed so that each axis executes serially. + The filter uses a butterfly diagram for each prime factor of the + dimension. This makes images with prime number dimensions (i.e. 17x17) + much slower to compute. Multi dimensional (i.e volumes) FFT's are + decomposed so that each axis executes serially. Parameters ---------- progress_bar : bool, optional Display a progress bar to indicate progress. - UniformGrid subset. Returns ------- pyvista.UniformGrid - UniformGrid subset. + UniformGrid with the applied reverse FFT. + + Examples + -------- + Apply reverse FFT to an example image. + + >>> from pyvista import examples + >>> image = examples.download_moonlanding_image() + >>> fft_image = image.fft() + >>> image_again = fft_image.rfft() + >>> image_again.point_data # doctest:+SKIP + pyvista DataSetAttributes + Association : POINT + Active Scalars : PNGImage + Active Vectors : None + Active Texture : None + Active Normals : None + Contains arrays : + PNGImage float64 (298620, 2) SCALARS + + See :ref:`image_fft_example` for a full example using this filter. + """ + self._check_fft_scalars() alg = _vtk.vtkImageRFFT() alg.SetInputDataObject(self) - _update_alg(alg, progress_bar, 'Reverse Fast Fourier Transform.') - result = _get_output(alg) - return result + _update_alg(alg, progress_bar, 'Performing Reverse Fast Fourier Transform.') + return _get_output(alg) + + def low_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): + """Perform a low pass filter in the frequency domain. + + This filter only works on an image after it has been converted to + frequency domain by a :func:`UniformGridFilters.fft` filter. + + A :func:`UniformGridFilters.rfft` filter can be + used to convert the output back into the spatial + domain. This filter attenuates high frequency components. + Input and output are in doubles, with two components. + + Parameters + ---------- + x_cutoff : double + The cutoff frequency for the x axis. + + y_cutoff : double + The cutoff frequency for the y axis. + + z_cutoff : double + The cutoff frequency for the z axis. + + order : int, optional + The order of the cutoff curve. Given from the equation + ``(1 + pow(CutOff/Freq(i, j), 2*Order))`` + + progress_bar : bool, optional + Display a progress bar to indicate progress. + + Returns + ------- + pyvista.UniformGrid + UniformGrid with the applied low pass filter + + Examples + -------- + See :ref:`image_fft_perlin_example` for a full example using this filter. + + """ + self._check_fft_scalars() + alg = _vtk.vtkImageButterworthLowPass() + alg.SetInputDataObject(self) + alg.SetCutOff(x_cutoff, y_cutoff, z_cutoff) + alg.SetOrder(1) + _update_alg(alg, progress_bar, 'Performing Low Pass Filter') + return _get_output(alg) + + def high_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): + """Perform a high pass filter in the frequency domain. + + This filter only works on an image after it has been converted to + frequency domain by a :func:`UniformGridFilters.fft` filter. + + A :func:`UniformGridFilters.rfft` filter can be + used to convert the output back into the spatial + domain. This filter attenuates low frequency components. + Input and output are in doubles, with two components. + + Parameters + ---------- + x_cutoff : double + The cutoff frequency for the x axis. + + y_cutoff : double + The cutoff frequency for the y axis. + + z_cutoff : double + The cutoff frequency for the z axis. + + order : int, optional + The order of the cutoff curve. Given from the equation + ``(1 + pow(CutOff/Freq(i, j), 2*Order))`` + + progress_bar : bool, optional + Display a progress bar to indicate progress. + + Returns + ------- + pyvista.UniformGrid + UniformGrid with the applied high pass filter + + Examples + -------- + See :ref:`image_fft_perlin_example` for a full example using this filter. + + """ + self._check_fft_scalars() + alg = _vtk.vtkImageButterworthHighPass() + alg.SetInputDataObject(self) + alg.SetCutOff(x_cutoff, y_cutoff, z_cutoff) + alg.SetOrder(1) + _update_alg(alg, progress_bar, 'Performing High Pass Filter') + return _get_output(alg) + + def _check_fft_scalars(self): + """Check for active scalars with two components. + + This is necessary for rfft, low_pass, and high_pass filters. + + """ + # check for active scalars, otherwise risk of segfault + if self.point_data.active_scalars_name is None: + raise ValueError('FFT filters require active point scalars') + + scalars = self.point_data.active_scalars + + meets_req = scalars.ndim == 2 and scalars.shape[1] == 2 + if not meets_req: + raise ValueError('Active scalars must contain 2 components for this FFT filter.') diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py index db510c5628..dfee770c5b 100644 --- a/pyvista/examples/downloads.py +++ b/pyvista/examples/downloads.py @@ -3881,3 +3881,33 @@ def download_cells_nd(load=True): # pragma: no cover """ return _download_and_read("cellsnd.ascii.inp", load=load) + + +def download_moonlanding_image(load=True): # pragma: no cover + """Download the moonlanding image. + + This is a noisy image originally obtained from `Scipy Lecture Notes + `_ and can be used to demonstrate a + low pass filter. + + Parameters + ---------- + load : bool, optional + Load the dataset after downloading it when ``True``. Set this + to ``False`` and only the filename will be returned. + + Returns + ------- + pyvista.UniformGrid or str + DataSet or filename depending on ``load``. + + Examples + -------- + >>> from pyvista import examples + >>> dataset = examples.download_moonlanding_image() + >>> dataset.plot(cpos='xy', cmap='gray', background='w', show_scalar_bar=False) + + See :ref:`image_fft_example` for a full example using this dataset. + + """ + return _download_and_read('moonlanding.png', load=load) diff --git a/tests/conftest.py b/tests/conftest.py index 43f6d1a8da..5089bdfaad 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -101,3 +101,10 @@ def pointset(): rng = default_rng(0) points = rng.random((10, 3)) return pyvista.PointSet(points) + + +@fixture() +def noise(): + freq = [10, 5, 0] + noise = pyvista.perlin_noise(1, freq, (0, 0, 0)) + return pyvista.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(20, 20, 1)) diff --git a/tests/test_grid.py b/tests/test_grid.py index 2f68cb85bd..df6c72ead7 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -766,23 +766,41 @@ def test_cast_uniform_to_rectilinear(): assert rectilinear.bounds == grid.bounds -def test_fft_and_rfft(): - grid = examples.download_puppy() - # Convert rgb to grayscale. - # https://en.wikipedia.org/wiki/Grayscale#Luma_coding_in_video_systems - r = grid['JPEGImage'][:, 0] - g = grid['JPEGImage'][:, 1] - b = grid['JPEGImage'][:, 2] - grid.clear_data() - grid['GrayScale'] = 0.299 * r + 0.587 * g + 0.114 * b - fft = grid.image_fft() - assert fft.n_points == grid.n_points - assert fft.n_arrays == grid.n_arrays - rfft = fft.image_rfft() - assert rfft.n_points == grid.n_points - assert rfft.n_arrays == grid.n_arrays - assert np.allclose(rfft['GrayScale'][:, 0], grid['GrayScale']) - assert np.allclose(rfft['GrayScale'][:, 1], 0.0) +def test_fft_and_rfft(noise): + grid = pyvista.UniformGrid(dims=(10, 10, 1)) + with pytest.raises(ValueError, match='active point scalars'): + grid.fft() + + full_pass = noise.fft().rfft() + assert full_pass['ImageScalars'].ndim == 2 + + # expect FFT and and RFFT to transform from time --> freq --> time domain + assert np.allclose(noise['scalars'], full_pass['ImageScalars'][:, 0]) + assert np.allclose(full_pass['ImageScalars'][:, 1], 0) + + +def test_low_pass(noise): + noise_no_scalars = noise.copy() + noise_no_scalars.clear_data() + with pytest.raises(ValueError, match='active point scalars'): + noise_no_scalars.low_pass(1, 1, 1) + + with pytest.raises(ValueError, match='2 components'): + noise.low_pass(1, 1, 1) + + out_zeros = noise.fft().low_pass(0, 0, 0) + assert np.allclose(out_zeros['ImageScalars'][1:], 0) + + out = noise.fft().low_pass(1, 1, 1) + assert not np.allclose(out['ImageScalars'][1:], 0) + + +def test_high_pass(noise): + out_zeros = noise.fft().high_pass(100000, 100000, 100000) + assert np.allclose(out_zeros['ImageScalars'][1:], 0) + + out = noise.fft().high_pass(10, 10, 10) + assert not np.allclose(out['ImageScalars'][1:], 0) @pytest.mark.parametrize('binary', [True, False]) From 069bad215561a911a86a251bc59a31a2c8a6b774 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 2 Jun 2022 23:05:18 -0600 Subject: [PATCH 07/51] fix docs --- pyvista/core/filters/uniform_grid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index b35960af9e..267f13f369 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -544,7 +544,7 @@ def low_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): order : int, optional The order of the cutoff curve. Given from the equation - ``(1 + pow(CutOff/Freq(i, j), 2*Order))`` + ``(1 + pow(CutOff/Freq(i, j), 2*Order))``. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -552,7 +552,7 @@ def low_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): Returns ------- pyvista.UniformGrid - UniformGrid with the applied low pass filter + UniformGrid with the applied low pass filter. Examples -------- @@ -591,7 +591,7 @@ def high_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): order : int, optional The order of the cutoff curve. Given from the equation - ``(1 + pow(CutOff/Freq(i, j), 2*Order))`` + ``(1 + pow(CutOff/Freq(i, j), 2*Order))``. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -599,7 +599,7 @@ def high_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): Returns ------- pyvista.UniformGrid - UniformGrid with the applied high pass filter + UniformGrid with the applied high pass filter. Examples -------- From b2c71b2b5c8641b8dc9b5f67180eba0cc2aed419 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Thu, 2 Jun 2022 23:28:47 -0600 Subject: [PATCH 08/51] add in text for plots --- examples/01-filter/image-fft-perlin-noise.py | 7 ++- pyvista/core/filters/uniform_grid.py | 65 +++++++++++++++++--- tests/test_grid.py | 4 ++ 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index dbb4726a1c..3eaa0a5c9e 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -32,7 +32,7 @@ sampled = pv.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(xdim, ydim, 1)) # plot the sampled noise -sampled.plot(cpos='xy', show_scalar_bar=False) +sampled.plot(cpos='xy', show_scalar_bar=False, text='Perlin Noise') ############################################################################### # Next, perform a FFT of the noise and plot the frequency content. @@ -62,6 +62,7 @@ pl.add_mesh(subset, cmap='gray', show_scalar_bar=False) pl.camera_position = 'xy' pl.show_bounds(xlabel='X Frequency', ylabel='Y Frequency') +pl.add_text('Frequency Domain of the Perlin Noise') pl.show() ############################################################################### @@ -75,7 +76,7 @@ low_pass = sampled_fft.low_pass(0.5, 0.5, 0.5).rfft() # remove the complex data low_pass['ImageScalars'] = low_pass['ImageScalars'][:, 0] -low_pass.plot(cpos='xy', show_scalar_bar=False) +low_pass.plot(cpos='xy', show_scalar_bar=False, text='Low Pass of the Perlin Noise') ############################################################################### @@ -89,4 +90,4 @@ high_pass_noise = sampled_fft.high_pass(5, 5, 5).rfft() high_pass_noise['ImageScalars'] = high_pass_noise['ImageScalars'][:, 0] -high_pass_noise.plot(cpos='xy', show_scalar_bar=False) +high_pass_noise.plot(cpos='xy', show_scalar_bar=False, text='High Pass of the Perlin Noise') diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 267f13f369..7991408d86 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -415,7 +415,7 @@ def image_threshold( _update_alg(alg, progress_bar, 'Performing Image Thresholding') return _get_output(alg) - def fft(self, progress_bar=False): + def fft(self, output_scalars_name='ImageScalars', progress_bar=False): """Apply the fast fourier transform to the active scalars. The input can have real or complex data in any components and data @@ -432,6 +432,10 @@ def fft(self, progress_bar=False): Parameters ---------- + output_scalars_name : str, optional + The name of the output scalars. By default this is + `'ImageScalars'`. + progress_bar : bool, optional Display a progress bar to indicate progress. @@ -467,9 +471,11 @@ def fft(self, progress_bar=False): alg = _vtk.vtkImageFFT() alg.SetInputDataObject(self) _update_alg(alg, progress_bar, 'Performing Fast Fourier Transform') - return _get_output(alg) + output = _get_output(alg) + self._change_output_scalars(output, output_scalars_name) + return output - def rfft(self, progress_bar=False): + def rfft(self, output_scalars_name='ImageScalars', progress_bar=False): """Apply the reverse fast fourier transform to the active scalars. The input can have real or complex data in any components and data @@ -485,6 +491,10 @@ def rfft(self, progress_bar=False): Parameters ---------- + output_scalars_name : str, optional + The name of the output scalars. By default this is + `'ImageScalars'`. + progress_bar : bool, optional Display a progress bar to indicate progress. @@ -518,9 +528,19 @@ def rfft(self, progress_bar=False): alg = _vtk.vtkImageRFFT() alg.SetInputDataObject(self) _update_alg(alg, progress_bar, 'Performing Reverse Fast Fourier Transform.') - return _get_output(alg) + output = _get_output(alg) + self._change_output_scalars(output, output_scalars_name) + return output - def low_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): + def low_pass( + self, + x_cutoff, + y_cutoff, + z_cutoff, + order=1, + output_scalars_name='ImageScalars', + progress_bar=False, + ): """Perform a low pass filter in the frequency domain. This filter only works on an image after it has been converted to @@ -546,6 +566,10 @@ def low_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): The order of the cutoff curve. Given from the equation ``(1 + pow(CutOff/Freq(i, j), 2*Order))``. + output_scalars_name : str, optional + The name of the output scalars. By default this is + `'ImageScalars'`. + progress_bar : bool, optional Display a progress bar to indicate progress. @@ -565,9 +589,19 @@ def low_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): alg.SetCutOff(x_cutoff, y_cutoff, z_cutoff) alg.SetOrder(1) _update_alg(alg, progress_bar, 'Performing Low Pass Filter') - return _get_output(alg) + output = _get_output(alg) + self._change_output_scalars(output, output_scalars_name) + return output - def high_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): + def high_pass( + self, + x_cutoff, + y_cutoff, + z_cutoff, + order=1, + output_scalars_name='ImageScalars', + progress_bar=False, + ): """Perform a high pass filter in the frequency domain. This filter only works on an image after it has been converted to @@ -593,6 +627,10 @@ def high_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): The order of the cutoff curve. Given from the equation ``(1 + pow(CutOff/Freq(i, j), 2*Order))``. + output_scalars_name : str, optional + The name of the output scalars. By default this is + `'ImageScalars'`. + progress_bar : bool, optional Display a progress bar to indicate progress. @@ -610,9 +648,18 @@ def high_pass(self, x_cutoff, y_cutoff, z_cutoff, order=1, progress_bar=False): alg = _vtk.vtkImageButterworthHighPass() alg.SetInputDataObject(self) alg.SetCutOff(x_cutoff, y_cutoff, z_cutoff) - alg.SetOrder(1) + alg.SetOrder(order) _update_alg(alg, progress_bar, 'Performing High Pass Filter') - return _get_output(alg) + output = _get_output(alg) + self._change_output_scalars(output, output_scalars_name) + return output + + def _change_output_scalars(self, dataset, name): + """Modify the name of the output scalars for a FFT filter.""" + default_name = 'ImageScalars' + if name != default_name: + pdata = dataset.point_data + dataset.point_data[name] = pdata.pop(default_name) def _check_fft_scalars(self): """Check for active scalars with two components. diff --git a/tests/test_grid.py b/tests/test_grid.py index df6c72ead7..088c1dc329 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -778,6 +778,10 @@ def test_fft_and_rfft(noise): assert np.allclose(noise['scalars'], full_pass['ImageScalars'][:, 0]) assert np.allclose(full_pass['ImageScalars'][:, 1], 0) + output_scalars_name = 'out_scalars' + noise_fft = noise.fft(output_scalars_name=output_scalars_name) + assert output_scalars_name in noise_fft.point_data + def test_low_pass(noise): noise_no_scalars = noise.copy() From 937b929095b95e8283222844e939fcd0a7ae3ae8 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 3 Jun 2022 08:34:32 -0600 Subject: [PATCH 09/51] add license info --- pyvista/examples/downloads.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py index dfee770c5b..3dff4a6738 100644 --- a/pyvista/examples/downloads.py +++ b/pyvista/examples/downloads.py @@ -3890,6 +3890,10 @@ def download_moonlanding_image(load=True): # pragma: no cover `_ and can be used to demonstrate a low pass filter. + See the `scipy-lectures license + `_ for more details + regarding this image's use and distribution. + Parameters ---------- load : bool, optional From 6225e577d3a1497813b7565f953dcfd6a7eb7674 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 4 Jun 2022 09:26:04 -0600 Subject: [PATCH 10/51] Apply suggestions from code review --- examples/01-filter/image-fft-perlin-noise.py | 7 +++---- pyvista/core/filters/uniform_grid.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 3eaa0a5c9e..d2c597fd33 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -17,6 +17,7 @@ """ import numpy as np +import pyvista as pv ############################################################################### # Start by generating some `Perlin Noise `_ @@ -24,7 +25,6 @@ # Note that we are generating it in a flat plane and using 10 Hz in the x # direction and 5 Hz in the y direction. # -import pyvista as pv freq = [10, 5, 0] noise = pv.perlin_noise(1, freq, (0, 0, 0)) @@ -74,8 +74,7 @@ # As expected, we only see low frequency noise. low_pass = sampled_fft.low_pass(0.5, 0.5, 0.5).rfft() -# remove the complex data -low_pass['ImageScalars'] = low_pass['ImageScalars'][:, 0] +low_pass['ImageScalars'] = low_pass['ImageScalars'][:, 0] # remove the complex data low_pass.plot(cpos='xy', show_scalar_bar=False, text='Low Pass of the Perlin Noise') @@ -89,5 +88,5 @@ # frequency noise has been attenuated. high_pass_noise = sampled_fft.high_pass(5, 5, 5).rfft() -high_pass_noise['ImageScalars'] = high_pass_noise['ImageScalars'][:, 0] +high_pass_noise['ImageScalars'] = high_pass_noise['ImageScalars'][:, 0] # remove the complex data high_pass_noise.plot(cpos='xy', show_scalar_bar=False, text='High Pass of the Perlin Noise') diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 7991408d86..a7ca38f271 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -434,7 +434,7 @@ def fft(self, output_scalars_name='ImageScalars', progress_bar=False): ---------- output_scalars_name : str, optional The name of the output scalars. By default this is - `'ImageScalars'`. + ``'ImageScalars'``. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -493,7 +493,7 @@ def rfft(self, output_scalars_name='ImageScalars', progress_bar=False): ---------- output_scalars_name : str, optional The name of the output scalars. By default this is - `'ImageScalars'`. + ``'ImageScalars'``. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -568,7 +568,7 @@ def low_pass( output_scalars_name : str, optional The name of the output scalars. By default this is - `'ImageScalars'`. + ``'ImageScalars'``. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -629,7 +629,7 @@ def high_pass( output_scalars_name : str, optional The name of the output scalars. By default this is - `'ImageScalars'`. + ``'ImageScalars'``. progress_bar : bool, optional Display a progress bar to indicate progress. From 8eda23eb1e4ac65e1723d79838e2b5ec2a26b977 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sat, 4 Jun 2022 09:41:52 -0600 Subject: [PATCH 11/51] make isort happy --- examples/01-filter/image-fft-perlin-noise.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index d2c597fd33..2a59f802d8 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -17,6 +17,7 @@ """ import numpy as np + import pyvista as pv ############################################################################### From 703a664c0a255d26d0f88972d93ba28cf91395de Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Sat, 4 Jun 2022 23:17:18 +0200 Subject: [PATCH 12/51] Apply obvious changes from code review --- examples/01-filter/image-fft-perlin-noise.py | 6 +++--- examples/01-filter/image-fft.py | 4 ++-- pyvista/core/filters/uniform_grid.py | 15 +++++++-------- pyvista/examples/downloads.py | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 2a59f802d8..c508243baa 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -8,7 +8,7 @@ :class:`pyvista.UniformGrid` using :func:`pyvista.UniformGridFilters.fft` filter. -Here, we demonstrate FFT usage by first generating perlin noise using +Here, we demonstrate FFT usage by first generating Perlin noise using :func:`pyvista.sample_function() ` to sample :func:`pyvista.perlin_noise `, and then performing FFT of the sampled noise to show the frequency content of @@ -70,7 +70,7 @@ # Low Pass Filter # ~~~~~~~~~~~~~~~ # For fun, let's perform a low pass filter on the frequency content and then -# convert it back into the "time" domain by immediately apply a reverse FFT. +# convert it back into the "time" domain by immediately applying a reverse FFT. # # As expected, we only see low frequency noise. @@ -83,7 +83,7 @@ # High Pass Filter # ~~~~~~~~~~~~~~~~ # This time, let's perform a high pass filter on the frequency content and then -# convert it back into the "time" domain by immediately apply a reverse FFT. +# convert it back into the "time" domain by immediately applying a reverse FFT. # # As expected, we only see the high frequency noise content as the low # frequency noise has been attenuated. diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index dc469de531..e60d40881f 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -9,7 +9,7 @@ filter. Here, we demonstrate FFT usage by denoising an image, effectively removing any -"high frequency" content by effectively performing a `low pass filter +"high frequency" content by performing a `low pass filter `_ This example was inspired by `Image denoising by FFT @@ -21,7 +21,7 @@ from pyvista import examples ############################################################################### -# Load the example image moon-landing image and plot it. +# Load the example Moon-landing image and plot it. image = examples.download_moonlanding_image() print(image.point_data) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index a7ca38f271..be542645d8 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -416,13 +416,12 @@ def image_threshold( return _get_output(alg) def fft(self, output_scalars_name='ImageScalars', progress_bar=False): - """Apply the fast fourier transform to the active scalars. + """Apply a fast Fourier transform (FFT) to the active scalars. The input can have real or complex data in any components and data types, but the output is always complex doubles, with the first component containing real values and the second component containing - imaginary values. The filter is fastest for images that have power of - two sizes. + imaginary values. The filter is fastest for images that have power of two sizes. The filter uses a butterfly diagram for each prime factor of the @@ -442,7 +441,7 @@ def fft(self, output_scalars_name='ImageScalars', progress_bar=False): Returns ------- pyvista.UniformGrid - UniformGrid with applied FFT. + :class:`pyvista.UniformGrid` with applied FFT. Examples -------- @@ -476,7 +475,7 @@ def fft(self, output_scalars_name='ImageScalars', progress_bar=False): return output def rfft(self, output_scalars_name='ImageScalars', progress_bar=False): - """Apply the reverse fast fourier transform to the active scalars. + """Apply the reverse fast Fourier transform (RFFT) to the active scalars. The input can have real or complex data in any components and data types, but the output is always complex doubles, with the first @@ -501,7 +500,7 @@ def rfft(self, output_scalars_name='ImageScalars', progress_bar=False): Returns ------- pyvista.UniformGrid - UniformGrid with the applied reverse FFT. + :class:`pyvista.UniformGrid` with the applied reverse FFT. Examples -------- @@ -576,7 +575,7 @@ def low_pass( Returns ------- pyvista.UniformGrid - UniformGrid with the applied low pass filter. + :class:`pyvista.UniformGrid` with the applied low pass filter. Examples -------- @@ -587,7 +586,7 @@ def low_pass( alg = _vtk.vtkImageButterworthLowPass() alg.SetInputDataObject(self) alg.SetCutOff(x_cutoff, y_cutoff, z_cutoff) - alg.SetOrder(1) + alg.SetOrder(order) _update_alg(alg, progress_bar, 'Performing Low Pass Filter') output = _get_output(alg) self._change_output_scalars(output, output_scalars_name) diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py index 3dff4a6738..362c351a5d 100644 --- a/pyvista/examples/downloads.py +++ b/pyvista/examples/downloads.py @@ -3903,7 +3903,7 @@ def download_moonlanding_image(load=True): # pragma: no cover Returns ------- pyvista.UniformGrid or str - DataSet or filename depending on ``load``. + ``DataSet`` or filename depending on ``load``. Examples -------- From a69ea28952b1bc412c95c30884a72eccf0f71596 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 11:00:15 -0600 Subject: [PATCH 13/51] Apply suggestions from code review Co-authored-by: Andras Deak --- pyvista/core/filters/uniform_grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index be542645d8..fe76ebc9cf 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -563,7 +563,7 @@ def low_pass( order : int, optional The order of the cutoff curve. Given from the equation - ``(1 + pow(CutOff/Freq(i, j), 2*Order))``. + ``1 + (cutoff/freq(i, j))**(2*order)``. output_scalars_name : str, optional The name of the output scalars. By default this is From e35997e539b5c3a799471a252e38a3b658a98beb Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Sun, 5 Jun 2022 23:03:47 +0200 Subject: [PATCH 14/51] Minor fixes in FFT examples --- examples/01-filter/image-fft-perlin-noise.py | 2 +- examples/01-filter/image-fft.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index c508243baa..dac39bbb23 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -21,7 +21,7 @@ import pyvista as pv ############################################################################### -# Start by generating some `Perlin Noise `_ +# Start by generating some `Perlin Noise `_. # # Note that we are generating it in a flat plane and using 10 Hz in the x # direction and 5 Hz in the y direction. diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index e60d40881f..2a61d1081f 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -10,7 +10,7 @@ Here, we demonstrate FFT usage by denoising an image, effectively removing any "high frequency" content by performing a `low pass filter -`_ +`_. This example was inspired by `Image denoising by FFT `_. @@ -21,7 +21,7 @@ from pyvista import examples ############################################################################### -# Load the example Moon-landing image and plot it. +# Load the example Moon landing image and plot it. image = examples.download_moonlanding_image() print(image.point_data) @@ -52,7 +52,7 @@ # .. note:: # VTK internally creates a normalized array to plot both the real and # imaginary values from the FFT filter. To avoid having this array included -# in the dataset, we use :func:`copy() ` to create a +# in the dataset, we use :func:`copy() ` to create a # temporary copy that's plotted. fft_image.copy().plot( From 2a04d0c555b8651127fdd1dfa43266ace414a6a4 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Sun, 5 Jun 2022 23:23:16 +0200 Subject: [PATCH 15/51] Fix Butterworth order formulae and indentation --- pyvista/core/filters/uniform_grid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index fe76ebc9cf..f7f6bd7461 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -563,7 +563,7 @@ def low_pass( order : int, optional The order of the cutoff curve. Given from the equation - ``1 + (cutoff/freq(i, j))**(2*order)``. + ``1 + (cutoff/freq(i, j))**(2*order)``. output_scalars_name : str, optional The name of the output scalars. By default this is @@ -624,7 +624,7 @@ def high_pass( order : int, optional The order of the cutoff curve. Given from the equation - ``(1 + pow(CutOff/Freq(i, j), 2*Order))``. + ``1 + (cutoff/freq(i, j))**(2*order)``. output_scalars_name : str, optional The name of the output scalars. By default this is From 09593b03f21a219d3c3de3a56493b0a3a900cb51 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 16:19:47 -0600 Subject: [PATCH 16/51] lots of excitement --- examples/01-filter/image-fft-perlin-noise.py | 10 +-- examples/01-filter/sampling_functions_2d.py | 5 +- pyvista/core/dataobject.py | 3 + pyvista/core/datasetattributes.py | 38 +++++++---- pyvista/core/filters/uniform_grid.py | 72 ++++++++++++-------- pyvista/core/imaging.py | 2 + pyvista/core/pyvista_ndarray.py | 51 +++++++++++++- pyvista/utilities/helpers.py | 26 ++++--- tests/conftest.py | 2 +- tests/test_datasetattributes.py | 15 ++++ tests/test_grid.py | 26 ++++--- tests/test_pyvista_ndarray.py | 9 +++ 12 files changed, 190 insertions(+), 69 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index dac39bbb23..bb2f789e92 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -21,15 +21,17 @@ import pyvista as pv ############################################################################### -# Start by generating some `Perlin Noise `_. +# Start by generating some `Perlin Noise `_ as in :ref:`perlin_noise_2d_example` example. # -# Note that we are generating it in a flat plane and using 10 Hz in the x -# direction and 5 Hz in the y direction. +# Note that we are generating it in a flat plane and using a frequency of 10 in +# the x direction and 5 in the y direction. Units of the frequency is ``1/pixel``. # +# Also note that the dimensions of the image are a power of 2. This is because +# FFT is an efficient implement of the discrete fourier transform. freq = [10, 5, 0] noise = pv.perlin_noise(1, freq, (0, 0, 0)) -xdim, ydim = (500, 500) +xdim, ydim = (512, 512) sampled = pv.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(xdim, ydim, 1)) # plot the sampled noise diff --git a/examples/01-filter/sampling_functions_2d.py b/examples/01-filter/sampling_functions_2d.py index 27df2b8549..4e4f94e09e 100644 --- a/examples/01-filter/sampling_functions_2d.py +++ b/examples/01-filter/sampling_functions_2d.py @@ -1,4 +1,6 @@ """ +.. _perlin_noise_2d_example: + Sample Function: Perlin Noise in 2D ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Here we use :func:`pyvista.core.imaging.sample_function` to sample @@ -6,8 +8,7 @@ Perlin noise is atype of gradient noise often used by visual effects artists to increase the appearance of realism in computer graphics. -Source: -https://en.wikipedia.org/wiki/Perlin_noise +Source: `Perlin Noise Wikipedia `_ The development of Perlin Noise has allowed computer graphics artists to better represent the complexity of natural phenomena in visual diff --git a/pyvista/core/dataobject.py b/pyvista/core/dataobject.py index ec7a9948b4..06bd002958 100644 --- a/pyvista/core/dataobject.py +++ b/pyvista/core/dataobject.py @@ -36,6 +36,9 @@ def __init__(self, *args, **kwargs) -> None: # conversion from bool to vtkBitArray, such arrays are stored as vtkCharArray. self.association_bitarray_names: DefaultDict = collections.defaultdict(set) + # view these arrays as complex128 + self._association_complex_names: DefaultDict = collections.defaultdict(set) + def __getattr__(self, item: str) -> Any: """Get attribute from base class if not found.""" return super().__getattribute__(item) diff --git a/pyvista/core/datasetattributes.py b/pyvista/core/datasetattributes.py index 461b8b94c7..db1c7926c4 100644 --- a/pyvista/core/datasetattributes.py +++ b/pyvista/core/datasetattributes.py @@ -145,7 +145,6 @@ def __repr__(self) -> str: for i, (name, array) in enumerate(self.items()): if len(name) > 23: name = f'{name[:20]}...' - # vtk_arr = array.VTKObject try: arr_type = attr_type[self.IsArrayAnAttribute(i)] except (IndexError, TypeError, AttributeError): # pragma: no cover @@ -156,9 +155,7 @@ def __repr__(self) -> str: if name == self.active_vectors_name: arr_type = 'VECTORS' - line = ( - f'{name[:23]:<24}{str(array.dtype):<9}{str(array.shape):<20} {arr_type}'.strip() - ) + line = f'{name[:23]:<24}{str(array.dtype):<11}{str(array.shape):<20} {arr_type}'.strip() lines.append(line) array_info = '\n ' + '\n '.join(lines) @@ -547,8 +544,16 @@ def get_array( if type(vtk_arr) == _vtk.vtkAbstractArray: return vtk_arr narray = pyvista_ndarray(vtk_arr, dataset=self.dataset, association=self.association) - if vtk_arr.GetName() in self.dataset.association_bitarray_names[self.association.name]: + + name = vtk_arr.GetName() + if name in self.dataset.association_bitarray_names[self.association.name]: narray = narray.view(np.bool_) # type: ignore + elif name in self.dataset._association_complex_names[self.association.name]: + narray = narray.view(np.complex128) # type: ignore + # ravel to keep this array flat to match the behavior of the rest + # of VTK arrays + if narray.ndim == 2 and narray.shape[-1] == 1: + narray = narray.ravel() return narray def set_array( @@ -584,6 +589,9 @@ def set_array( dataset. Note that this will automatically become the active scalars. + Complex arrays will be represented internally as a 2 component float64 + array. This is due to limitations of VTK's native datatypes. + Examples -------- Add a point array to a mesh. @@ -771,6 +779,18 @@ def _prepare_array( if data.dtype == np.bool_: self.dataset.association_bitarray_names[self.association.name].add(name) data = data.view(np.uint8) + elif np.issubdtype(data.dtype, np.complexfloating): + if data.dtype != np.complex128: + raise ValueError('Only numpy.complex128 is supported when setting data attributes') + + if data.ndim != 1: + if data.shape[1] != 1: + raise ValueError('Complex data must be single dimensional.') + self.dataset._association_complex_names[self.association.name].add(name) + + # complex data is stored internally as a contiguous 2 component + # float64 array + data = data.view(np.float64).reshape(-1, 2) shape = data.shape if data.ndim == 3: @@ -804,7 +824,6 @@ def _prepare_array( # output. We want to make sure that the array added to the output is not # referring to the input dataset. copy = pyvista_ndarray(data) - return helpers.convert_array(copy, name, deep=deep_copy) def append( @@ -1000,12 +1019,7 @@ def values(self) -> List[pyvista_ndarray]: [pyvista_ndarray([0, 0, 0, 0, 0, 0]), pyvista_ndarray([0, 1, 2, 3, 4, 5])] """ - values = [] - for name in self.keys(): - array = self.VTKObject.GetAbstractArray(name) - arr = pyvista_ndarray(array, dataset=self.dataset, association=self.association) - values.append(arr) - return values + return [self.get_array(name) for name in self.keys()] def clear(self): """Remove all arrays in this object. diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index f7f6bd7461..361257b984 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -415,13 +415,12 @@ def image_threshold( _update_alg(alg, progress_bar, 'Performing Image Thresholding') return _get_output(alg) - def fft(self, output_scalars_name='ImageScalars', progress_bar=False): - """Apply a fast Fourier transform (FFT) to the active scalars. + def fft(self, output_scalars_name=None, progress_bar=False): + """Apply the fast fourier transform to the active scalars. The input can have real or complex data in any components and data - types, but the output is always complex doubles, with the first - component containing real values and the second component containing - imaginary values. + types, but the output is always complex128. The filter is fastest for + images that have power of two sizes. The filter is fastest for images that have power of two sizes. The filter uses a butterfly diagram for each prime factor of the @@ -432,8 +431,8 @@ def fft(self, output_scalars_name='ImageScalars', progress_bar=False): Parameters ---------- output_scalars_name : str, optional - The name of the output scalars. By default this is - ``'ImageScalars'``. + The name of the output scalars. By default this is the same as the + active scalars of the dataset. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -458,7 +457,7 @@ def fft(self, output_scalars_name='ImageScalars', progress_bar=False): Active Texture : None Active Normals : None Contains arrays : - PNGImage float64 (298620, 2) SCALARS + PNGImage complex128 (298620,) SCALARS See :ref:`image_fft_example` for a full example using this filter. @@ -471,11 +470,13 @@ def fft(self, output_scalars_name='ImageScalars', progress_bar=False): alg.SetInputDataObject(self) _update_alg(alg, progress_bar, 'Performing Fast Fourier Transform') output = _get_output(alg) - self._change_output_scalars(output, output_scalars_name) + self._change_fft_output_scalars( + output, self.point_data.active_scalars_name, output_scalars_name + ) return output - def rfft(self, output_scalars_name='ImageScalars', progress_bar=False): - """Apply the reverse fast Fourier transform (RFFT) to the active scalars. + def rfft(self, output_scalars_name=None, progress_bar=False): + """Apply the reverse fast fourier transform (RFFT) to the active scalars. The input can have real or complex data in any components and data types, but the output is always complex doubles, with the first @@ -491,8 +492,8 @@ def rfft(self, output_scalars_name='ImageScalars', progress_bar=False): Parameters ---------- output_scalars_name : str, optional - The name of the output scalars. By default this is - ``'ImageScalars'``. + The name of the output scalars. By default this is the same as the + active scalars of the dataset. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -528,7 +529,9 @@ def rfft(self, output_scalars_name='ImageScalars', progress_bar=False): alg.SetInputDataObject(self) _update_alg(alg, progress_bar, 'Performing Reverse Fast Fourier Transform.') output = _get_output(alg) - self._change_output_scalars(output, output_scalars_name) + self._change_fft_output_scalars( + output, self.point_data.active_scalars_name, output_scalars_name + ) return output def low_pass( @@ -537,7 +540,7 @@ def low_pass( y_cutoff, z_cutoff, order=1, - output_scalars_name='ImageScalars', + output_scalars_name=None, progress_bar=False, ): """Perform a low pass filter in the frequency domain. @@ -566,8 +569,8 @@ def low_pass( ``1 + (cutoff/freq(i, j))**(2*order)``. output_scalars_name : str, optional - The name of the output scalars. By default this is - ``'ImageScalars'``. + The name of the output scalars. By default this is the same as the + active scalars of the dataset. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -589,7 +592,9 @@ def low_pass( alg.SetOrder(order) _update_alg(alg, progress_bar, 'Performing Low Pass Filter') output = _get_output(alg) - self._change_output_scalars(output, output_scalars_name) + self._change_fft_output_scalars( + output, self.point_data.active_scalars_name, output_scalars_name + ) return output def high_pass( @@ -598,7 +603,7 @@ def high_pass( y_cutoff, z_cutoff, order=1, - output_scalars_name='ImageScalars', + output_scalars_name=None, progress_bar=False, ): """Perform a high pass filter in the frequency domain. @@ -627,8 +632,8 @@ def high_pass( ``1 + (cutoff/freq(i, j))**(2*order)``. output_scalars_name : str, optional - The name of the output scalars. By default this is - ``'ImageScalars'``. + The name of the output scalars. By default this is the same as the + active scalars of the dataset. progress_bar : bool, optional Display a progress bar to indicate progress. @@ -650,15 +655,20 @@ def high_pass( alg.SetOrder(order) _update_alg(alg, progress_bar, 'Performing High Pass Filter') output = _get_output(alg) - self._change_output_scalars(output, output_scalars_name) + self._change_fft_output_scalars( + output, self.point_data.active_scalars_name, output_scalars_name + ) return output - def _change_output_scalars(self, dataset, name): - """Modify the name of the output scalars for a FFT filter.""" - default_name = 'ImageScalars' - if name != default_name: - pdata = dataset.point_data - dataset.point_data[name] = pdata.pop(default_name) + def _change_fft_output_scalars(self, dataset, orig_name, out_name): + """Modify the name and dtype of the output scalars for a FFT filter.""" + name = orig_name if out_name is None else out_name + pdata = dataset.point_data + if pdata.active_scalars_name != name: + pdata[name] = pdata.pop(pdata.active_scalars_name) + + # always view the datatype of the point_data as complex128 + dataset._association_complex_names['POINT'].add(name) def _check_fft_scalars(self): """Check for active scalars with two components. @@ -674,4 +684,8 @@ def _check_fft_scalars(self): meets_req = scalars.ndim == 2 and scalars.shape[1] == 2 if not meets_req: - raise ValueError('Active scalars must contain 2 components for this FFT filter.') + raise ValueError( + 'Active scalars must be complex data for this filter, represented ' + 'either as a 2 component float array, or as an array with ' + 'dtype=numpy.complex128' + ) diff --git a/pyvista/core/imaging.py b/pyvista/core/imaging.py index 6bd04bfd55..2daa90468f 100644 --- a/pyvista/core/imaging.py +++ b/pyvista/core/imaging.py @@ -111,6 +111,8 @@ def sample_function( >>> surf = pyvista.sample_function(noise, dim=(200, 200, 1)) >>> surf.plot() + See :ref:`perlin_noise_2d_example` for a full example using this function. + """ samp = _vtk.vtkSampleFunction() samp.SetImplicitFunction(function) diff --git a/pyvista/core/pyvista_ndarray.py b/pyvista/core/pyvista_ndarray.py index 3bd1cbef5d..aa6e473560 100644 --- a/pyvista/core/pyvista_ndarray.py +++ b/pyvista/core/pyvista_ndarray.py @@ -1,5 +1,6 @@ """Contains pyvista_ndarray a numpy ndarray type used in pyvista.""" from collections.abc import Iterable +from functools import wraps from typing import Union import numpy as np @@ -59,7 +60,7 @@ def __array_finalize__(self, obj): def __setitem__(self, key: Union[int, np.ndarray], value): """Implement [] set operator. - When the array is changed it triggers "Modified()" which updates + When the array is changed it triggers ``Modified()`` which updates all upstream objects, including any render windows holding the object. """ @@ -72,4 +73,52 @@ def __setitem__(self, key: Union[int, np.ndarray], value): if dataset is not None and dataset.Get(): dataset.Get().Modified() + @wraps(np.max) + def max(self, *args, **kwargs): + """Wrap numpy.max to return a single value when applicable.""" + output = super().max(*args, **kwargs) + if output.shape == (): + return output.item(0) + return output + + @wraps(np.mean) + def mean(self, *args, **kwargs): + """Wrap numpy.mean to return a single value when applicable.""" + output = super().mean(*args, **kwargs) + if output.shape == (): + return output.item(0) + return output + + @wraps(np.sum) + def sum(self, *args, **kwargs): + """Wrap numpy.sum to return a single value when applicable.""" + output = super().sum(*args, **kwargs) + if output.shape == (): + return output.item(0) + return output + + @wraps(np.min) + def min(self, *args, **kwargs): + """Wrap numpy.min to return a single value when applicable.""" + output = super().min(*args, **kwargs) + if output.shape == (): + return output.item(0) + return output + + @wraps(np.std) + def std(self, *args, **kwargs): + """Wrap numpy.std to return a single value when applicable.""" + output = super().std(*args, **kwargs) + if output.shape == (): + return output.item(0) + return output + + @wraps(np.prod) + def prod(self, *args, **kwargs): + """Wrap numpy.prod to return a single value when applicable.""" + output = super().prod(*args, **kwargs) + if output.shape == (): + return output.item(0) + return output + __getattr__ = _vtk.VTKArray.__getattr__ diff --git a/pyvista/utilities/helpers.py b/pyvista/utilities/helpers.py index 53cb49f299..f805e51212 100644 --- a/pyvista/utilities/helpers.py +++ b/pyvista/utilities/helpers.py @@ -213,12 +213,14 @@ def point_array(obj, name): Returns ------- - numpy.ndarray - Wrapped array. + pyvista.ndarray or None + Wrapped array if available. """ - vtkarr = obj.GetPointData().GetAbstractArray(name) - return convert_array(vtkarr) + try: + return obj.point_data.get_array(name) + except KeyError: + return def field_array(obj, name): @@ -234,12 +236,14 @@ def field_array(obj, name): Returns ------- - numpy.ndarray + pyvista.ndarray Wrapped array. """ - vtkarr = obj.GetFieldData().GetAbstractArray(name) - return convert_array(vtkarr) + try: + return obj.field_data.get_array(name) + except KeyError: + return def cell_array(obj, name): @@ -255,12 +259,14 @@ def cell_array(obj, name): Returns ------- - numpy.ndarray + pyvista.ndarray Wrapped array. """ - vtkarr = obj.GetCellData().GetAbstractArray(name) - return convert_array(vtkarr) + try: + return obj.cell_data.get_array(name) + except KeyError: + return def row_array(obj, name): diff --git a/tests/conftest.py b/tests/conftest.py index 5089bdfaad..974e7f506e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -107,4 +107,4 @@ def pointset(): def noise(): freq = [10, 5, 0] noise = pyvista.perlin_noise(1, freq, (0, 0, 0)) - return pyvista.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(20, 20, 1)) + return pyvista.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(2**4, 2**4, 1)) diff --git a/tests/test_datasetattributes.py b/tests/test_datasetattributes.py index e550b45650..fd84dec7d9 100644 --- a/tests/test_datasetattributes.py +++ b/tests/test_datasetattributes.py @@ -517,3 +517,18 @@ def test_active_t_coords_name(plane): with raises(AttributeError): plane.field_data.active_t_coords_name = 'arr' + + +def test_complex(plane): + """Test if complex data can be properly represented in datasetattributes.""" + with raises(ValueError, match='Only numpy.complex128'): + plane.point_data['my_data'] = np.empty(plane.n_points, dtype=np.complex64) + + with raises(ValueError, match='Complex data must be single dimensional'): + plane.point_data['my_data'] = np.empty((plane.n_points, 2), dtype=np.complex128) + + data = np.random.random((plane.n_points, 2)).view(np.complex128).ravel() + plane.point_data['my_data'] = data + assert np.allclose(plane.point_data['my_data'], data) + + assert 'complex128' in str(plane.point_data) diff --git a/tests/test_grid.py b/tests/test_grid.py index 088c1dc329..ccd7564a03 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -771,40 +771,46 @@ def test_fft_and_rfft(noise): with pytest.raises(ValueError, match='active point scalars'): grid.fft() + name = noise.active_scalars_name + noise_fft = noise.fft() + assert noise_fft[name].dtype == np.complex128 + full_pass = noise.fft().rfft() - assert full_pass['ImageScalars'].ndim == 2 + assert full_pass[name].dtype == np.complex128 # expect FFT and and RFFT to transform from time --> freq --> time domain - assert np.allclose(noise['scalars'], full_pass['ImageScalars'][:, 0]) - assert np.allclose(full_pass['ImageScalars'][:, 1], 0) + assert np.allclose(noise['scalars'], full_pass[name].real) + assert np.allclose(full_pass[name].imag, 0) output_scalars_name = 'out_scalars' noise_fft = noise.fft(output_scalars_name=output_scalars_name) assert output_scalars_name in noise_fft.point_data -def test_low_pass(noise): +def test_fft_low_pass(noise): + name = noise.active_scalars_name noise_no_scalars = noise.copy() noise_no_scalars.clear_data() with pytest.raises(ValueError, match='active point scalars'): noise_no_scalars.low_pass(1, 1, 1) - with pytest.raises(ValueError, match='2 components'): + with pytest.raises(ValueError, match='must be complex data'): noise.low_pass(1, 1, 1) out_zeros = noise.fft().low_pass(0, 0, 0) - assert np.allclose(out_zeros['ImageScalars'][1:], 0) + assert np.allclose(out_zeros[name][1:], 0) out = noise.fft().low_pass(1, 1, 1) - assert not np.allclose(out['ImageScalars'][1:], 0) + assert not np.allclose(out[name][1:], 0) -def test_high_pass(noise): +def test_fft_high_pass(noise): + name = noise.active_scalars_name out_zeros = noise.fft().high_pass(100000, 100000, 100000) - assert np.allclose(out_zeros['ImageScalars'][1:], 0) + assert np.allclose(out_zeros[name][1:], 0) out = noise.fft().high_pass(10, 10, 10) - assert not np.allclose(out['ImageScalars'][1:], 0) + assert not np.allclose(out[name][1:], 0) @pytest.mark.parametrize('binary', [True, False]) diff --git a/tests/test_pyvista_ndarray.py b/tests/test_pyvista_ndarray.py index 706c793ad0..e07c02d168 100644 --- a/tests/test_pyvista_ndarray.py +++ b/tests/test_pyvista_ndarray.py @@ -64,3 +64,12 @@ def test_slices_are_associated_single_index(): assert points[1, 1].VTKObject == points.VTKObject assert points[1, 1].dataset.Get() == points.dataset.Get() assert points[1, 1].association == points.association + + +def test_methods_return_float(): + # ensure that methods like np.sum return float just like numpy + arr = pyvista_ndarray([1.2, 1.3]) + # breakpoint() + # arr.max() + assert isinstance(arr.max(), float) + assert isinstance(arr.sum(), float) From 34c2b02ab67460361c4f435946159212b8d29dbf Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 17:08:57 -0600 Subject: [PATCH 17/51] fix docstring --- examples/01-filter/image-fft.py | 2 +- pyvista/core/dataobject.py | 4 ++-- pyvista/core/dataset.py | 16 ++++++++-------- pyvista/core/datasetattributes.py | 28 ++++++++++++++-------------- pyvista/core/filters/data_set.py | 2 +- pyvista/core/filters/poly_data.py | 2 +- pyvista/examples/downloads.py | 4 ++-- pyvista/examples/examples.py | 4 ++-- pyvista/utilities/reader.py | 12 ++++++------ 9 files changed, 37 insertions(+), 37 deletions(-) diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index 2a61d1081f..1392e669a7 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -75,7 +75,7 @@ per_keep = 0.10 width, height, _ = fft_image.dimensions -data = fft_image['PNGImage'].reshape(height, width, 2) # note: axes flipped +data = fft_image['PNGImage'].reshape(height, width) # note: axes flipped data[int(height * per_keep) : -int(height * per_keep)] = 0 data[:, int(width * per_keep) : -int(width * per_keep)] = 0 diff --git a/pyvista/core/dataobject.py b/pyvista/core/dataobject.py index 06bd002958..db166b38ae 100644 --- a/pyvista/core/dataobject.py +++ b/pyvista/core/dataobject.py @@ -333,7 +333,7 @@ def add_field_data(self, array: np.ndarray, name: str, deep=True): >>> mesh = pyvista.Sphere() >>> mesh.add_field_data(np.arange(10), 'my-field-data') >>> mesh['my-field-data'] - array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Add field data to a UniformGrid dataset. @@ -341,7 +341,7 @@ def add_field_data(self, array: np.ndarray, name: str, deep=True): >>> mesh.add_field_data(['I could', 'write', 'notes', 'here'], ... 'my-field-data') >>> mesh['my-field-data'] - array(['I could', 'write', 'notes', 'here'], dtype='>> cube['my_array'] = range(cube.n_points) >>> cube.rename_array('my_array', 'my_renamed_array') >>> cube['my_renamed_array'] - array([0, 1, 2, 3, 4, 5, 6, 7]) + pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7]) """ field = get_array_association(self, old_name, preference=preference) @@ -1449,8 +1449,8 @@ def point_data(self) -> DataSetAttributes: Active Texture : None Active Normals : None Contains arrays : - my_array float64 (8,) - my_other_array int64 (8,) SCALARS + my_array float64 (8,) + my_other_array int64 (8,) SCALARS Access an array from ``point_data``. @@ -1586,8 +1586,8 @@ def cell_data(self) -> DataSetAttributes: Active Texture : None Active Normals : None Contains arrays : - my_array float64 (6,) - my_other_array int64 (6,) SCALARS + my_array float64 (6,) + my_other_array int64 (6,) SCALARS Access an array from ``cell_data``. @@ -1782,17 +1782,17 @@ def get_array( Get the point data array. >>> mesh.get_array('point-data') - array([0, 1, 2, 3, 4, 5, 6, 7]) + pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7]) Get the cell data array. >>> mesh.get_array('cell-data') - array([0, 1, 2, 3, 4, 5]) + pyvista_ndarray([0, 1, 2, 3, 4, 5]) Get the field data array. >>> mesh.get_array('field-data') - array(['a', 'b', 'c'], dtype=' Optional[pyvista_ndarray]: Active Texture : TextureCoordinates Active Normals : Normals Contains arrays : - Normals float32 (4, 3) NORMALS - TextureCoordinates float32 (4, 2) TCOORDS + Normals float32 (4, 3) NORMALS + TextureCoordinates float32 (4, 2) TCOORDS >>> mesh.point_data.active_normals pyvista_ndarray([[0.000000e+00, 0.000000e+00, -1.000000e+00], @@ -1231,7 +1231,7 @@ def active_normals(self) -> Optional[pyvista_ndarray]: Active Texture : None Active Normals : Normals Contains arrays : - Normals float64 (1, 3) NORMALS + Normals float64 (1, 3) NORMALS """ self._raise_no_normals() diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py index a7314d9f73..b7219aff5f 100644 --- a/pyvista/core/filters/data_set.py +++ b/pyvista/core/filters/data_set.py @@ -320,7 +320,7 @@ def compute_implicit_distance(self, surface, inplace=False): >>> _ = sphere.compute_implicit_distance(plane, inplace=True) >>> dist = sphere['implicit_distance'] >>> type(dist) - + Plot these distances as a heatmap diff --git a/pyvista/core/filters/poly_data.py b/pyvista/core/filters/poly_data.py index 69c6c4fcde..af98bdf336 100644 --- a/pyvista/core/filters/poly_data.py +++ b/pyvista/core/filters/poly_data.py @@ -2937,7 +2937,7 @@ def collision( >>> mesh_b = pyvista.Cube((0.5, 0.5, 0.5)).extract_cells([0, 2, 4]) >>> collision, ncol = mesh_a.collision(mesh_b, cell_tolerance=1) >>> collision['ContactCells'][:10] - array([471, 471, 468, 468, 469, 469, 466, 466, 467, 467]) + pyvista_ndarray([471, 471, 468, 468, 469, 469, 466, 466, 467, 467]) Plot the collisions by creating a collision mask with the ``"ContactCells"`` field data. Cells with a collision are diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py index 362c351a5d..b8c00da4a1 100644 --- a/pyvista/examples/downloads.py +++ b/pyvista/examples/downloads.py @@ -3217,8 +3217,8 @@ def download_mars_jpg(): # pragma: no cover Active Texture : Texture Coordinates Active Normals : Normals Contains arrays : - Normals float32 (14280, 3) NORMALS - Texture Coordinates float64 (14280, 2) TCOORDS + Normals float32 (14280, 3) NORMALS + Texture Coordinates float64 (14280, 2) TCOORDS Plot with stars in the background. diff --git a/pyvista/examples/examples.py b/pyvista/examples/examples.py index 6e4d77745e..2ef8a09768 100755 --- a/pyvista/examples/examples.py +++ b/pyvista/examples/examples.py @@ -299,8 +299,8 @@ def load_sphere_vectors(): Active Texture : None Active Normals : Normals Contains arrays : - Normals float32 (842, 3) NORMALS - vectors float32 (842, 3) VECTORS + Normals float32 (842, 3) NORMALS + vectors float32 (842, 3) VECTORS """ sphere = pyvista.Sphere(radius=3.14) diff --git a/pyvista/utilities/reader.py b/pyvista/utilities/reader.py index f25b5e0fde..bdd0b77de2 100644 --- a/pyvista/utilities/reader.py +++ b/pyvista/utilities/reader.py @@ -1223,12 +1223,12 @@ class CGNSReader(BaseReader, PointCellDataSelection): Active Texture : None Active Normals : None Contains arrays : - Density float64 (2928,) - Momentum float64 (2928, 3) VECTORS - EnergyStagnationDensity float64 (2928,) - ViscosityEddy float64 (2928,) - TurbulentDistance float64 (2928,) - TurbulentSANuTilde float64 (2928,) + Density float64 (2928,) + Momentum float64 (2928, 3) VECTORS + EnergyStagnationDensity float64 (2928,) + ViscosityEddy float64 (2928,) + TurbulentDistance float64 (2928,) + TurbulentSANuTilde float64 (2928,) """ From cb4ee6166be3410d1852078a341106d5f5c139ea Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 20:27:27 -0600 Subject: [PATCH 18/51] cleanup complex --- examples/01-filter/image-fft-perlin-noise.py | 49 +++++++++++++++----- examples/01-filter/image-fft.py | 14 ++++-- pyvista/core/datasetattributes.py | 5 ++ pyvista/core/pyvista_ndarray.py | 3 ++ pyvista/plotting/mapper.py | 6 ++- pyvista/utilities/geometric_objects.py | 2 +- tests/plotting/test_plotting.py | 10 ++++ tests/test_datasetattributes.py | 35 ++++++++++++-- 8 files changed, 103 insertions(+), 21 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index bb2f789e92..3f2028adde 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -21,13 +21,16 @@ import pyvista as pv ############################################################################### -# Start by generating some `Perlin Noise `_ as in :ref:`perlin_noise_2d_example` example. +# Start by generating some `Perlin Noise +# `_ as in +# :ref:`perlin_noise_2d_example` example. # # Note that we are generating it in a flat plane and using a frequency of 10 in -# the x direction and 5 in the y direction. Units of the frequency is ``1/pixel``. +# the x direction and 5 in the y direction. Units of the frequency is +# ``1/pixel``. # # Also note that the dimensions of the image are a power of 2. This is because -# FFT is an efficient implement of the discrete fourier transform. +# the FFT is most efficient for images dimensioned in a power of 2. freq = [10, 5, 0] noise = pv.perlin_noise(1, freq, (0, 0, 0)) @@ -71,25 +74,49 @@ ############################################################################### # Low Pass Filter # ~~~~~~~~~~~~~~~ -# For fun, let's perform a low pass filter on the frequency content and then -# convert it back into the "time" domain by immediately applying a reverse FFT. +# Let's perform a low pass filter on the frequency content and then convert it +# back into the space (pixel) domain by immediately applying a reverse FFT. +# +# When converting back, keep only the real content. The imaginary content has no +# physical meaning in the physical domain. # # As expected, we only see low frequency noise. -low_pass = sampled_fft.low_pass(0.5, 0.5, 0.5).rfft() -low_pass['ImageScalars'] = low_pass['ImageScalars'][:, 0] # remove the complex data +low_pass = sampled_fft.low_pass(1.0, 1.0, 1.0).rfft() +low_pass['scalars'] = low_pass.active_scalars.real low_pass.plot(cpos='xy', show_scalar_bar=False, text='Low Pass of the Perlin Noise') +# low_pass.plot(scalars=low_pass.active_scalars.real, cpos='xy', show_scalar_bar=True, text='Low Pass of the Perlin Noise') ############################################################################### # High Pass Filter # ~~~~~~~~~~~~~~~~ # This time, let's perform a high pass filter on the frequency content and then -# convert it back into the "time" domain by immediately applying a reverse FFT. +# convert it back into the space (pixel) domain by immediately applying a +# reverse FFT. +# +# When converting back, keep only the real content. The imaginary content has no +# physical meaning in the pixel domain. # # As expected, we only see the high frequency noise content as the low # frequency noise has been attenuated. -high_pass_noise = sampled_fft.high_pass(5, 5, 5).rfft() -high_pass_noise['ImageScalars'] = high_pass_noise['ImageScalars'][:, 0] # remove the complex data -high_pass_noise.plot(cpos='xy', show_scalar_bar=False, text='High Pass of the Perlin Noise') +high_pass = sampled_fft.high_pass(1.0, 1.0, 1.0).rfft() +high_pass['scalars'] = high_pass.active_scalars.real +high_pass.plot(cpos='xy', show_scalar_bar=False, text='High Pass of the Perlin Noise') + +############################################################################### +# Show that the low and high passes of the original noise sum to the original. + +grid = pv.UniformGrid(dims=(xdim, ydim, 1)) +grid['scalars'] = high_pass['scalars'] + low_pass['scalars'] + +pl = pv.Plotter(shape=(1, 2)) +pl.add_mesh(sampled, show_scalar_bar=False) +pl.add_text('Original Dataset') +pl.camera_position = 'xy' +pl.subplot(0, 1) +pl.add_mesh(grid, show_scalar_bar=False) +pl.add_text('Summed Low and High Passes') +pl.camera_position = 'xy' +pl.show() diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index 1392e669a7..acb3c31836 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -36,9 +36,10 @@ ############################################################################### # Apply FFT to the image # -# FFT will be applied to the active scalars, which is stored in ``'PNGImage'``. -# The output from the filter contains both real and imaginary components and is -# stored in the same array. +# FFT will be applied to the active scalars, which is stored as ``'PNGImage'``, +# the default scalars name when loading a PNG image. +# +# The output from the filter is a complex array. fft_image = image.fft() fft_image.point_data @@ -71,6 +72,11 @@ # # Next, perform a low pass filter by removing the middle 80% of the content of # the image. Note that the high frequency content is in the middle of the array. +# +# .. note:: +# It is easier and more efficient to use the existing +# :func:`pyvista.UniformGridFilters.low_pass` filter. This section is here +# for demonstration purposes. per_keep = 0.10 @@ -88,7 +94,7 @@ ############################################################################### -# Finally, convert the image data back to the "image domain" and plot it. +# Finally, convert the image data back to the "spacial" domain and plot it. rfft = fft_image.rfft() rfft.plot(cpos="xy", theme=grey_theme, text='Processed Moon Landing Image') diff --git a/pyvista/core/datasetattributes.py b/pyvista/core/datasetattributes.py index a67b5c71a3..f873edd17d 100644 --- a/pyvista/core/datasetattributes.py +++ b/pyvista/core/datasetattributes.py @@ -619,6 +619,11 @@ def set_array( """ vtk_arr = self._prepare_array(data, name, deep_copy) + complex_assoc = self.dataset._association_complex_names + if name in complex_assoc[self.association.name]: + if isinstance(data, np.ndarray): + if not np.issubdtype(data.dtype, complex): + complex_assoc[self.association.name].remove(name) self.VTKObject.AddArray(vtk_arr) self.VTKObject.Modified() diff --git a/pyvista/core/pyvista_ndarray.py b/pyvista/core/pyvista_ndarray.py index aa6e473560..5e59a30d22 100644 --- a/pyvista/core/pyvista_ndarray.py +++ b/pyvista/core/pyvista_ndarray.py @@ -121,4 +121,7 @@ def prod(self, *args, **kwargs): return output.item(0) return output + # def __del__(self): + # del self.dataset + __getattr__ = _vtk.VTKArray.__getattr__ diff --git a/pyvista/plotting/mapper.py b/pyvista/plotting/mapper.py index f8c054dfc8..ed2da1ddae 100644 --- a/pyvista/plotting/mapper.py +++ b/pyvista/plotting/mapper.py @@ -93,7 +93,6 @@ def set_scalars( _using_labels = False if not np.issubdtype(scalars.dtype, np.number): - # raise TypeError('Non-numeric scalars are currently not supported for plotting.') # TODO: If str array, digitive and annotate cats, scalars = np.unique(scalars.astype('|S'), return_inverse=True) values = np.unique(scalars) @@ -103,6 +102,11 @@ def set_scalars( scalar_bar_args.setdefault('n_labels', 0) _using_labels = True + # Convert complex to real if applicable + if np.issubdtype(scalars.dtype, complex): + scalars = np.abs(scalars) + title = title + '-Normed' + if rgb: show_scalar_bar = False if scalars.ndim != 2 or scalars.shape[1] < 3 or scalars.shape[1] > 4: diff --git a/pyvista/utilities/geometric_objects.py b/pyvista/utilities/geometric_objects.py index e8616b787f..abca1cdf62 100644 --- a/pyvista/utilities/geometric_objects.py +++ b/pyvista/utilities/geometric_objects.py @@ -627,7 +627,7 @@ def Cube(center=(0.0, 0.0, 0.0), x_length=1.0, y_length=1.0, z_length=1.0, bound # add face index data for compatibility with PlatonicSolid # but make it inactive for backwards compatibility - cube.cell_data.set_array([1, 4, 0, 3, 5, 2], ['FaceIndex']) + cube.cell_data.set_array([1, 4, 0, 3, 5, 2], 'FaceIndex') # clean duplicate points if clean: diff --git a/tests/plotting/test_plotting.py b/tests/plotting/test_plotting.py index add61a6f92..4b8c3b4343 100644 --- a/tests/plotting/test_plotting.py +++ b/tests/plotting/test_plotting.py @@ -2315,3 +2315,13 @@ def test_ruler(sphere): plotter.add_ruler([-0.6, -0.6, 0], [0.6, -0.6, 0], font_size_factor=1.2) plotter.view_xy() plotter.show(before_close_callback=verify_cache_image) + + +def test_plot_complex_value(plane): + """Test plotting complex data.""" + data = np.arange(plane.n_points * 2).astype(np.float64).reshape(-1, 2).view(np.complex128) + plane.plot(scalars=data) + + pl = pyvista.Plotter() + pl.add_mesh(plane, scalars=data, show_scalar_bar=False) + pl.show(before_close_callback=verify_cache_image) diff --git a/tests/test_datasetattributes.py b/tests/test_datasetattributes.py index fd84dec7d9..f277c7a9d7 100644 --- a/tests/test_datasetattributes.py +++ b/tests/test_datasetattributes.py @@ -1,5 +1,6 @@ from string import ascii_letters, digits, whitespace import sys +import weakref from hypothesis import HealthCheck, given, settings from hypothesis.extra.numpy import arrays @@ -521,14 +522,40 @@ def test_active_t_coords_name(plane): def test_complex(plane): """Test if complex data can be properly represented in datasetattributes.""" + name = 'my_data' with raises(ValueError, match='Only numpy.complex128'): - plane.point_data['my_data'] = np.empty(plane.n_points, dtype=np.complex64) + plane.point_data[name] = np.empty(plane.n_points, dtype=np.complex64) with raises(ValueError, match='Complex data must be single dimensional'): - plane.point_data['my_data'] = np.empty((plane.n_points, 2), dtype=np.complex128) + plane.point_data[name] = np.empty((plane.n_points, 2), dtype=np.complex128) data = np.random.random((plane.n_points, 2)).view(np.complex128).ravel() - plane.point_data['my_data'] = data - assert np.allclose(plane.point_data['my_data'], data) + plane.point_data[name] = data + assert np.allclose(plane.point_data[name], data) assert 'complex128' in str(plane.point_data) + + # test setter + plane.active_scalars_name = name + + # ensure that association is removed when changing datatype + assert plane.point_data[name].dtype == np.complex128 + plane.point_data[name] = plane.point_data[name].real + assert np.issubdtype(plane.point_data[name].dtype, float) + + +def test_complex_collection(plane): + name = 'my_data' + data = np.random.random((plane.n_points, 2)).view(np.complex128).ravel() + plane.point_data[name] = data + + # ensure shallow copy + data[0] += 1 + data_copy = data.copy() + assert np.allclose(plane.point_data[name], data) + + # ensure references remain + ref = weakref.ref(data) + del data + assert np.allclose(plane.point_data[name], data_copy) + assert ref is not None From 6a8a1e3b6d951adbfdc66b069931949e126f498e Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 20:58:28 -0600 Subject: [PATCH 19/51] add image cache file --- pyvista/plotting/mapper.py | 2 +- .../plotting/image_cache/plot_complex_value.png | Bin 0 -> 16436 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/plotting/image_cache/plot_complex_value.png diff --git a/pyvista/plotting/mapper.py b/pyvista/plotting/mapper.py index ed2da1ddae..9850c0aba5 100644 --- a/pyvista/plotting/mapper.py +++ b/pyvista/plotting/mapper.py @@ -105,7 +105,7 @@ def set_scalars( # Convert complex to real if applicable if np.issubdtype(scalars.dtype, complex): scalars = np.abs(scalars) - title = title + '-Normed' + title = f'{title}-normed' if rgb: show_scalar_bar = False diff --git a/tests/plotting/image_cache/plot_complex_value.png b/tests/plotting/image_cache/plot_complex_value.png new file mode 100644 index 0000000000000000000000000000000000000000..6b1123a90fc7ce7cf10893a603eff522b6f9129d GIT binary patch literal 16436 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 literal 0 HcmV?d00001 From 3c9a3740d6462e1d102a37d5fbbc6d2702ffd667 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 21:26:20 -0600 Subject: [PATCH 20/51] fixture: noise --> noise_2d --- tests/conftest.py | 2 +- tests/test_grid.py | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 974e7f506e..71720bf2fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,7 +104,7 @@ def pointset(): @fixture() -def noise(): +def noise_2d(): freq = [10, 5, 0] noise = pyvista.perlin_noise(1, freq, (0, 0, 0)) return pyvista.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(2**4, 2**4, 1)) diff --git a/tests/test_grid.py b/tests/test_grid.py index dfefac7318..5c25d67096 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -777,50 +777,50 @@ def test_cast_uniform_to_rectilinear(): assert rectilinear.bounds == grid.bounds -def test_fft_and_rfft(noise): +def test_fft_and_rfft(noise_2d): grid = pyvista.UniformGrid(dims=(10, 10, 1)) with pytest.raises(ValueError, match='active point scalars'): grid.fft() - name = noise.active_scalars_name - noise_fft = noise.fft() + name = noise_2d.active_scalars_name + noise_fft = noise_2d.fft() assert noise_fft[name].dtype == np.complex128 - full_pass = noise.fft().rfft() + full_pass = noise_2d.fft().rfft() assert full_pass[name].dtype == np.complex128 # expect FFT and and RFFT to transform from time --> freq --> time domain - assert np.allclose(noise['scalars'], full_pass[name].real) + assert np.allclose(noise_2d['scalars'], full_pass[name].real) assert np.allclose(full_pass[name].imag, 0) output_scalars_name = 'out_scalars' - noise_fft = noise.fft(output_scalars_name=output_scalars_name) + noise_fft = noise_2d.fft(output_scalars_name=output_scalars_name) assert output_scalars_name in noise_fft.point_data -def test_fft_low_pass(noise): - name = noise.active_scalars_name - noise_no_scalars = noise.copy() +def test_fft_low_pass(noise_2d): + name = noise_2d.active_scalars_name + noise_no_scalars = noise_2d.copy() noise_no_scalars.clear_data() with pytest.raises(ValueError, match='active point scalars'): noise_no_scalars.low_pass(1, 1, 1) with pytest.raises(ValueError, match='must be complex data'): - noise.low_pass(1, 1, 1) + noise_2d.low_pass(1, 1, 1) - out_zeros = noise.fft().low_pass(0, 0, 0) + out_zeros = noise_2d.fft().low_pass(0, 0, 0) assert np.allclose(out_zeros[name][1:], 0) - out = noise.fft().low_pass(1, 1, 1) + out = noise_2d.fft().low_pass(1, 1, 1) assert not np.allclose(out[name][1:], 0) -def test_fft_high_pass(noise): - name = noise.active_scalars_name - out_zeros = noise.fft().high_pass(100000, 100000, 100000) +def test_fft_high_pass(noise_2d): + name = noise_2d.active_scalars_name + out_zeros = noise_2d.fft().high_pass(100000, 100000, 100000) assert np.allclose(out_zeros[name][1:], 0) - out = noise.fft().high_pass(10, 10, 10) + out = noise_2d.fft().high_pass(10, 10, 10) assert not np.allclose(out[name][1:], 0) From 4b7fbe1a00b8c893d26ad82d5300fdc733cf8831 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 21:43:09 -0600 Subject: [PATCH 21/51] cast to float like matplotlib --- pyvista/plotting/mapper.py | 6 +++--- tests/plotting/test_plotting.py | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyvista/plotting/mapper.py b/pyvista/plotting/mapper.py index 9850c0aba5..865f93420a 100644 --- a/pyvista/plotting/mapper.py +++ b/pyvista/plotting/mapper.py @@ -102,10 +102,10 @@ def set_scalars( scalar_bar_args.setdefault('n_labels', 0) _using_labels = True - # Convert complex to real if applicable + # Use only the real component if an array is complex if np.issubdtype(scalars.dtype, complex): - scalars = np.abs(scalars) - title = f'{title}-normed' + scalars = scalars.astype(float) + title = f'{title}-real' if rgb: show_scalar_bar = False diff --git a/tests/plotting/test_plotting.py b/tests/plotting/test_plotting.py index 72aa0f381f..2c75c27f47 100644 --- a/tests/plotting/test_plotting.py +++ b/tests/plotting/test_plotting.py @@ -2338,9 +2338,12 @@ def test_ruler(sphere): def test_plot_complex_value(plane): """Test plotting complex data.""" - data = np.arange(plane.n_points * 2).astype(np.float64).reshape(-1, 2).view(np.complex128) - plane.plot(scalars=data) + data = np.arange(plane.n_points, dtype=np.complex128) + data += np.linspace(0, 1, plane.n_points) * -1j + with pytest.warns(np.ComplexWarning): + plane.plot(scalars=data) pl = pyvista.Plotter() - pl.add_mesh(plane, scalars=data, show_scalar_bar=False) + with pytest.warns(np.ComplexWarning): + pl.add_mesh(plane, scalars=data, show_scalar_bar=False) pl.show(before_close_callback=verify_cache_image) From bb85f9f7d1141450eca7b24c4dba6a8b26b5581c Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 5 Jun 2022 22:45:05 -0600 Subject: [PATCH 22/51] improve example --- examples/01-filter/image-fft-perlin-noise.py | 81 +++++++++++++++----- pyvista/core/dataobject.py | 2 +- pyvista/core/dataset.py | 1 + pyvista/core/datasetattributes.py | 6 +- 4 files changed, 67 insertions(+), 23 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 3f2028adde..4dee6d10d1 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -30,15 +30,17 @@ # ``1/pixel``. # # Also note that the dimensions of the image are a power of 2. This is because -# the FFT is most efficient for images dimensioned in a power of 2. +# the FFT is much more efficient for arrays sized as a power of 2. freq = [10, 5, 0] noise = pv.perlin_noise(1, freq, (0, 0, 0)) -xdim, ydim = (512, 512) +xdim, ydim = (2**9, 2**9) sampled = pv.sample_function(noise, bounds=(0, 10, 0, 10, 0, 10), dim=(xdim, ydim, 1)) -# plot the sampled noise -sampled.plot(cpos='xy', show_scalar_bar=False, text='Perlin Noise') +# warp and plot the sampled noise +warped_noise = sampled.warp_by_scalar() +warped_noise.plot(show_scalar_bar=False, text='Perlin Noise', lighting=False) + ############################################################################### # Next, perform a FFT of the noise and plot the frequency content. @@ -64,28 +66,38 @@ # frequency content in the x direction and this matches the frequencies given # to :func:`pyvista.perlin_noise `. -pl = pv.Plotter() -pl.add_mesh(subset, cmap='gray', show_scalar_bar=False) -pl.camera_position = 'xy' -pl.show_bounds(xlabel='X Frequency', ylabel='Y Frequency') +# scale by log to make the plot viewable +subset['scalars'] = np.abs(subset.active_scalars.real) +warped_subset = subset.warp_by_scalar(factor=0.001) + +pl = pv.Plotter(lighting='three lights') +pl.add_mesh(warped_subset, cmap='blues', show_scalar_bar=False) +pl.show_bounds( + xlabel='X Frequency', + ylabel='Y Frequency', + zlabel='Amplitude', + color='k', +) pl.add_text('Frequency Domain of the Perlin Noise') pl.show() + ############################################################################### # Low Pass Filter # ~~~~~~~~~~~~~~~ # Let's perform a low pass filter on the frequency content and then convert it # back into the space (pixel) domain by immediately applying a reverse FFT. # -# When converting back, keep only the real content. The imaginary content has no -# physical meaning in the physical domain. +# When converting back, keep only the real content. The imaginary content has +# no physical meaning in the physical domain. PyVista will drop the imaginary +# content, but will warn you of it. # # As expected, we only see low frequency noise. low_pass = sampled_fft.low_pass(1.0, 1.0, 1.0).rfft() low_pass['scalars'] = low_pass.active_scalars.real -low_pass.plot(cpos='xy', show_scalar_bar=False, text='Low Pass of the Perlin Noise') -# low_pass.plot(scalars=low_pass.active_scalars.real, cpos='xy', show_scalar_bar=True, text='Low Pass of the Perlin Noise') +warped_low_pass = low_pass.warp_by_scalar() +warped_low_pass.plot(show_scalar_bar=False, text='Low Pass of the Perlin Noise', lighting=False) ############################################################################### @@ -103,20 +115,51 @@ high_pass = sampled_fft.high_pass(1.0, 1.0, 1.0).rfft() high_pass['scalars'] = high_pass.active_scalars.real -high_pass.plot(cpos='xy', show_scalar_bar=False, text='High Pass of the Perlin Noise') +warped_high_pass = high_pass.warp_by_scalar() +warped_high_pass.plot(show_scalar_bar=False, text='High Pass of the Perlin Noise', lighting=False) + ############################################################################### -# Show that the low and high passes of the original noise sum to the original. +# Show that the sum of the low and high passes equal the original noise. -grid = pv.UniformGrid(dims=(xdim, ydim, 1)) +grid = pv.UniformGrid(dims=sampled.dimensions, spacing=sampled.spacing) grid['scalars'] = high_pass['scalars'] + low_pass['scalars'] pl = pv.Plotter(shape=(1, 2)) -pl.add_mesh(sampled, show_scalar_bar=False) +pl.add_mesh(sampled.warp_by_scalar(), show_scalar_bar=False, lighting=False) pl.add_text('Original Dataset') -pl.camera_position = 'xy' pl.subplot(0, 1) -pl.add_mesh(grid, show_scalar_bar=False) +pl.add_mesh(grid.warp_by_scalar(), show_scalar_bar=False, lighting=False) pl.add_text('Summed Low and High Passes') -pl.camera_position = 'xy' pl.show() + + +############################################################################### +# Animate +# ~~~~~~~ +# Animate the variation of the cutoff frequency. + + +def warp_low_pass_noise(cfreq): + """Process the sampled FFT and warp by scalars.""" + output = sampled_fft.low_pass(cfreq, cfreq, cfreq).rfft() + output['scalars'] = output.active_scalars.real + return output.warp_by_scalar() + + +# initialize the plotter and plot off-screen +plotter = pv.Plotter(notebook=False, off_screen=True) +plotter.open_gif("low_pass.gif", fps=8) + +# add the initial mesh +init_mesh = warp_low_pass_noise(1e-2) +plotter.add_mesh(init_mesh, show_scalar_bar=False, lighting=False) + +for freq in np.logspace(-2, 1, 25): + mesh = warp_low_pass_noise(freq) + plotter.add_mesh(mesh, show_scalar_bar=False, lighting=False) + plotter.add_text(f"Cutoff Frequency: {freq:.2f}", color="black") + plotter.write_frame() + plotter.clear() + +plotter.close() diff --git a/pyvista/core/dataobject.py b/pyvista/core/dataobject.py index db166b38ae..ee7f5ed862 100644 --- a/pyvista/core/dataobject.py +++ b/pyvista/core/dataobject.py @@ -34,7 +34,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__() # Remember which arrays come from numpy.bool arrays, because there is no direct # conversion from bool to vtkBitArray, such arrays are stored as vtkCharArray. - self.association_bitarray_names: DefaultDict = collections.defaultdict(set) + self._association_bitarray_names: DefaultDict = collections.defaultdict(set) # view these arrays as complex128 self._association_complex_names: DefaultDict = collections.defaultdict(set) diff --git a/pyvista/core/dataset.py b/pyvista/core/dataset.py index 5ffe68e34f..b1f4796e38 100644 --- a/pyvista/core/dataset.py +++ b/pyvista/core/dataset.py @@ -1412,6 +1412,7 @@ def copy_meta_from(self, ido: 'DataSet'): self._active_vectors_info = ido.active_vectors_info self.clear_textures() self._textures = {name: tex.copy() for name, tex in ido.textures.items()} + self._association_complex_names = ido._association_complex_names @property def point_arrays(self) -> DataSetAttributes: # pragma: no cover diff --git a/pyvista/core/datasetattributes.py b/pyvista/core/datasetattributes.py index f873edd17d..5f8a4366af 100644 --- a/pyvista/core/datasetattributes.py +++ b/pyvista/core/datasetattributes.py @@ -546,7 +546,7 @@ def get_array( narray = pyvista_ndarray(vtk_arr, dataset=self.dataset, association=self.association) name = vtk_arr.GetName() - if name in self.dataset.association_bitarray_names[self.association.name]: + if name in self.dataset._association_bitarray_names[self.association.name]: narray = narray.view(np.bool_) # type: ignore elif name in self.dataset._association_complex_names[self.association.name]: narray = narray.view(np.complex128) # type: ignore @@ -782,7 +782,7 @@ def _prepare_array( raise ValueError(f'data length of ({data.shape[0]}) != required length ({array_len})') if data.dtype == np.bool_: - self.dataset.association_bitarray_names[self.association.name].add(name) + self.dataset._association_bitarray_names[self.association.name].add(name) data = data.view(np.uint8) elif np.issubdtype(data.dtype, np.complexfloating): if data.dtype != np.complex128: @@ -900,7 +900,7 @@ def remove(self, key: str) -> None: raise KeyError(f'{key} not present.') try: - self.dataset.association_bitarray_names[self.association.name].remove(key) + self.dataset._association_bitarray_names[self.association.name].remove(key) except KeyError: pass self.VTKObject.RemoveArray(key) From 2f6af78a085bcf4aecf65c4995d50b2af05af3a1 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Mon, 6 Jun 2022 02:21:06 -0600 Subject: [PATCH 23/51] copy set --- pyvista/core/dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvista/core/dataset.py b/pyvista/core/dataset.py index b1f4796e38..a3b297437a 100644 --- a/pyvista/core/dataset.py +++ b/pyvista/core/dataset.py @@ -1412,7 +1412,7 @@ def copy_meta_from(self, ido: 'DataSet'): self._active_vectors_info = ido.active_vectors_info self.clear_textures() self._textures = {name: tex.copy() for name, tex in ido.textures.items()} - self._association_complex_names = ido._association_complex_names + self._association_complex_names = ido._association_complex_names.copy() @property def point_arrays(self) -> DataSetAttributes: # pragma: no cover From 66567ae50c94ecc900636b13ed857b5415172d73 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Mon, 6 Jun 2022 07:54:05 -0600 Subject: [PATCH 24/51] add header titles --- examples/01-filter/image-fft-perlin-noise.py | 10 +++++++- examples/01-filter/image-fft.py | 24 ++++++++++++-------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 4dee6d10d1..5203d9e4c4 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -13,7 +13,7 @@ sample :func:`pyvista.perlin_noise `, and then performing FFT of the sampled noise to show the frequency content of that noise. - +n """ import numpy as np @@ -21,6 +21,8 @@ import pyvista as pv ############################################################################### +# Generate Perlin Noise +# ~~~~~~~~~~~~~~~~~~~~~ # Start by generating some `Perlin Noise # `_ as in # :ref:`perlin_noise_2d_example` example. @@ -43,6 +45,8 @@ ############################################################################### +# Perform FFT of Perlin Noise +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Next, perform a FFT of the noise and plot the frequency content. # For the sake of simplicity, we will only plot the content in the first # quadrant. @@ -62,6 +66,8 @@ ############################################################################### +# Plot the Frequency Domain +# ~~~~~~~~~~~~~~~~~~~~~~~~~ # Now, plot the noise in the frequency domain. Note how there is more high # frequency content in the x direction and this matches the frequencies given # to :func:`pyvista.perlin_noise `. @@ -120,6 +126,8 @@ ############################################################################### +# Sum Low and High Pass +# ~~~~~~~~~~~~~~~~~~~~~ # Show that the sum of the low and high passes equal the original noise. grid = pv.UniformGrid(dims=sampled.dimensions, spacing=sampled.spacing) diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index acb3c31836..5fa5c95838 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -35,20 +35,22 @@ ############################################################################### # Apply FFT to the image +# ~~~~~~~~~~~~~~~~~~~~~~ +# FFT will be applied to the active scalars, ``'PNGImage'``, the default +# scalars name when loading a PNG image. # -# FFT will be applied to the active scalars, which is stored as ``'PNGImage'``, -# the default scalars name when loading a PNG image. -# -# The output from the filter is a complex array. +# The output from the filter is a complex array stored as the same array unless +# specified using ``output_scalars_name``. fft_image = image.fft() fft_image.point_data ############################################################################### -# Plot the FFT of the image. Note that we are effectively viewing the -# "frequency" of the data in this image, where the four corners contain the low -# frequency content of the image, and the middle is the high frequency content -# of the image. +# Plot the FFT of the image. +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Note that we are effectively viewing the "frequency" of the data in this +# image, where the four corners contain the low frequency content of the image, +# and the middle is the high frequency content of the image. # # .. note:: # VTK internally creates a normalized array to plot both the real and @@ -64,8 +66,8 @@ ) ############################################################################### -# Remove the noise from the fft_image -# +# Remove the noise from the ``fft_image`` +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Effectively, we want to remove high frequency (noisy) data from our image. # First, let's reshape by the size of the image. Note that the image data is in # real and imaginary axes. @@ -94,6 +96,8 @@ ############################################################################### +# Convert to the spatial domain using reverse FFT +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Finally, convert the image data back to the "spacial" domain and plot it. rfft = fft_image.rfft() From e7e949d17ed7f6a758739e88b67f40e71dfa0174 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 12 Jun 2022 01:20:06 -0600 Subject: [PATCH 25/51] remove complex and return float features --- pyvista/core/dataobject.py | 9 ++-- pyvista/core/dataset.py | 33 +++++++----- pyvista/core/datasetattributes.py | 75 ++++++++++---------------- pyvista/core/filters/data_set.py | 2 +- pyvista/core/filters/poly_data.py | 2 +- pyvista/core/pyvista_ndarray.py | 54 +------------------ pyvista/plotting/mapper.py | 6 +-- pyvista/utilities/geometric_objects.py | 7 +-- pyvista/utilities/helpers.py | 26 ++++----- pyvista/utilities/reader.py | 14 ++--- tests/plotting/test_plotting.py | 13 ----- tests/test_datasetattributes.py | 42 --------------- tests/test_pyvista_ndarray.py | 9 ---- 13 files changed, 76 insertions(+), 216 deletions(-) diff --git a/pyvista/core/dataobject.py b/pyvista/core/dataobject.py index ee7f5ed862..ec7a9948b4 100644 --- a/pyvista/core/dataobject.py +++ b/pyvista/core/dataobject.py @@ -34,10 +34,7 @@ def __init__(self, *args, **kwargs) -> None: super().__init__() # Remember which arrays come from numpy.bool arrays, because there is no direct # conversion from bool to vtkBitArray, such arrays are stored as vtkCharArray. - self._association_bitarray_names: DefaultDict = collections.defaultdict(set) - - # view these arrays as complex128 - self._association_complex_names: DefaultDict = collections.defaultdict(set) + self.association_bitarray_names: DefaultDict = collections.defaultdict(set) def __getattr__(self, item: str) -> Any: """Get attribute from base class if not found.""" @@ -333,7 +330,7 @@ def add_field_data(self, array: np.ndarray, name: str, deep=True): >>> mesh = pyvista.Sphere() >>> mesh.add_field_data(np.arange(10), 'my-field-data') >>> mesh['my-field-data'] - pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) + array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) Add field data to a UniformGrid dataset. @@ -341,7 +338,7 @@ def add_field_data(self, array: np.ndarray, name: str, deep=True): >>> mesh.add_field_data(['I could', 'write', 'notes', 'here'], ... 'my-field-data') >>> mesh['my-field-data'] - pyvista_ndarray(['I could', 'write', 'notes', 'here'], dtype='>> cube['my_array'] = range(cube.n_points) >>> cube.rename_array('my_array', 'my_renamed_array') >>> cube['my_renamed_array'] - pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7]) + array([0, 1, 2, 3, 4, 5, 6, 7]) """ field = get_array_association(self, old_name, preference=preference) @@ -1143,7 +1143,10 @@ def translate( ) def scale( - self, xyz: Union[list, tuple, np.ndarray], transform_all_input_vectors=False, inplace=False + self, + xyz: Union[Number, list, tuple, np.ndarray], + transform_all_input_vectors=False, + inplace=False, ): """Scale the mesh. @@ -1154,8 +1157,10 @@ def scale( Parameters ---------- - xyz : scale factor list or tuple or np.ndarray - Length 3 list, tuple or array. + xyz : float or list or tuple or np.ndarray + A scalar or length 3 list, tuple or array defining the scale + factors along x, y, and z. If a scalar, the same uniform scale is + used along all three axes. transform_all_input_vectors : bool, optional When ``True``, all input vectors are @@ -1187,6 +1192,9 @@ def scale( >>> _ = pl.add_mesh(mesh2) >>> pl.show(cpos="xy") """ + if isinstance(xyz, (float, int, np.number)): + xyz = [xyz] * 3 + transform = _vtk.vtkTransform() transform.Scale(xyz) return self.transform( @@ -1412,7 +1420,6 @@ def copy_meta_from(self, ido: 'DataSet'): self._active_vectors_info = ido.active_vectors_info self.clear_textures() self._textures = {name: tex.copy() for name, tex in ido.textures.items()} - self._association_complex_names = ido._association_complex_names.copy() @property def point_arrays(self) -> DataSetAttributes: # pragma: no cover @@ -1450,8 +1457,8 @@ def point_data(self) -> DataSetAttributes: Active Texture : None Active Normals : None Contains arrays : - my_array float64 (8,) - my_other_array int64 (8,) SCALARS + my_array float64 (8,) + my_other_array int64 (8,) SCALARS Access an array from ``point_data``. @@ -1587,8 +1594,8 @@ def cell_data(self) -> DataSetAttributes: Active Texture : None Active Normals : None Contains arrays : - my_array float64 (6,) - my_other_array int64 (6,) SCALARS + my_array float64 (6,) + my_other_array int64 (6,) SCALARS Access an array from ``cell_data``. @@ -1783,17 +1790,17 @@ def get_array( Get the point data array. >>> mesh.get_array('point-data') - pyvista_ndarray([0, 1, 2, 3, 4, 5, 6, 7]) + array([0, 1, 2, 3, 4, 5, 6, 7]) Get the cell data array. >>> mesh.get_array('cell-data') - pyvista_ndarray([0, 1, 2, 3, 4, 5]) + array([0, 1, 2, 3, 4, 5]) Get the field data array. >>> mesh.get_array('field-data') - pyvista_ndarray(['a', 'b', 'c'], dtype=' str: for i, (name, array) in enumerate(self.items()): if len(name) > 23: name = f'{name[:20]}...' + # vtk_arr = array.VTKObject try: arr_type = attr_type[self.IsArrayAnAttribute(i)] except (IndexError, TypeError, AttributeError): # pragma: no cover @@ -155,7 +156,9 @@ def __repr__(self) -> str: if name == self.active_vectors_name: arr_type = 'VECTORS' - line = f'{name[:23]:<24}{str(array.dtype):<11}{str(array.shape):<20} {arr_type}'.strip() + line = ( + f'{name[:23]:<24}{str(array.dtype):<9}{str(array.shape):<20} {arr_type}'.strip() + ) lines.append(line) array_info = '\n ' + '\n '.join(lines) @@ -544,16 +547,8 @@ def get_array( if type(vtk_arr) == _vtk.vtkAbstractArray: return vtk_arr narray = pyvista_ndarray(vtk_arr, dataset=self.dataset, association=self.association) - - name = vtk_arr.GetName() - if name in self.dataset._association_bitarray_names[self.association.name]: + if vtk_arr.GetName() in self.dataset.association_bitarray_names[self.association.name]: narray = narray.view(np.bool_) # type: ignore - elif name in self.dataset._association_complex_names[self.association.name]: - narray = narray.view(np.complex128) # type: ignore - # ravel to keep this array flat to match the behavior of the rest - # of VTK arrays - if narray.ndim == 2 and narray.shape[-1] == 1: - narray = narray.ravel() return narray def set_array( @@ -589,9 +584,6 @@ def set_array( dataset. Note that this will automatically become the active scalars. - Complex arrays will be represented internally as a 2 component float64 - array. This is due to limitations of VTK's native datatypes. - Examples -------- Add a point array to a mesh. @@ -619,11 +611,6 @@ def set_array( """ vtk_arr = self._prepare_array(data, name, deep_copy) - complex_assoc = self.dataset._association_complex_names - if name in complex_assoc[self.association.name]: - if isinstance(data, np.ndarray): - if not np.issubdtype(data.dtype, complex): - complex_assoc[self.association.name].remove(name) self.VTKObject.AddArray(vtk_arr) self.VTKObject.Modified() @@ -672,7 +659,7 @@ def set_scalars( Active Texture : None Active Normals : None Contains arrays : - my-scalars int64 (8,) SCALARS + my-scalars int64 (8,) SCALARS """ vtk_arr = self._prepare_array(scalars, name, deep_copy) @@ -735,7 +722,7 @@ def set_vectors( Active Texture : None Active Normals : None Contains arrays : - my-vectors float64 (8, 3) VECTORS + my-vectors float64 (8, 3) VECTORS """ # prepare the array and add an attribute so that we can track this as a vector @@ -782,20 +769,8 @@ def _prepare_array( raise ValueError(f'data length of ({data.shape[0]}) != required length ({array_len})') if data.dtype == np.bool_: - self.dataset._association_bitarray_names[self.association.name].add(name) + self.dataset.association_bitarray_names[self.association.name].add(name) data = data.view(np.uint8) - elif np.issubdtype(data.dtype, np.complexfloating): - if data.dtype != np.complex128: - raise ValueError('Only numpy.complex128 is supported when setting data attributes') - - if data.ndim != 1: - if data.shape[1] != 1: - raise ValueError('Complex data must be single dimensional.') - self.dataset._association_complex_names[self.association.name].add(name) - - # complex data is stored internally as a contiguous 2 component - # float64 array - data = data.view(np.float64).reshape(-1, 2) shape = data.shape if data.ndim == 3: @@ -829,6 +804,7 @@ def _prepare_array( # output. We want to make sure that the array added to the output is not # referring to the input dataset. copy = pyvista_ndarray(data) + return helpers.convert_array(copy, name, deep=deep_copy) def append( @@ -900,7 +876,7 @@ def remove(self, key: str) -> None: raise KeyError(f'{key} not present.') try: - self.dataset._association_bitarray_names[self.association.name].remove(key) + self.dataset.association_bitarray_names[self.association.name].remove(key) except KeyError: pass self.VTKObject.RemoveArray(key) @@ -1024,7 +1000,12 @@ def values(self) -> List[pyvista_ndarray]: [pyvista_ndarray([0, 0, 0, 0, 0, 0]), pyvista_ndarray([0, 1, 2, 3, 4, 5])] """ - return [self.get_array(name) for name in self.keys()] + values = [] + for name in self.keys(): + array = self.VTKObject.GetAbstractArray(name) + arr = pyvista_ndarray(array, dataset=self.dataset, association=self.association) + values.append(arr) + return values def clear(self): """Remove all arrays in this object. @@ -1080,9 +1061,9 @@ def update(self, array_dict: Union[Dict[str, np.ndarray], 'DataSetAttributes']): Active Texture : None Active Normals : None Contains arrays : - Spatial Point Data float64 (1000,) - foo int64 (1000,) - rand float64 (1000,) SCALARS + Spatial Point Data float64 (1000,) + foo int64 (1000,) + rand float64 (1000,) SCALARS """ for name, array in array_dict.items(): @@ -1214,8 +1195,8 @@ def active_normals(self) -> Optional[pyvista_ndarray]: Active Texture : TextureCoordinates Active Normals : Normals Contains arrays : - Normals float32 (4, 3) NORMALS - TextureCoordinates float32 (4, 2) TCOORDS + Normals float32 (4, 3) NORMALS + TextureCoordinates float32 (4, 2) TCOORDS >>> mesh.point_data.active_normals pyvista_ndarray([[0.000000e+00, 0.000000e+00, -1.000000e+00], @@ -1236,7 +1217,7 @@ def active_normals(self) -> Optional[pyvista_ndarray]: Active Texture : None Active Normals : Normals Contains arrays : - Normals float64 (1, 3) NORMALS + Normals float64 (1, 3) NORMALS """ self._raise_no_normals() diff --git a/pyvista/core/filters/data_set.py b/pyvista/core/filters/data_set.py index f521a03f0b..3e7cf94fde 100644 --- a/pyvista/core/filters/data_set.py +++ b/pyvista/core/filters/data_set.py @@ -320,7 +320,7 @@ def compute_implicit_distance(self, surface, inplace=False): >>> _ = sphere.compute_implicit_distance(plane, inplace=True) >>> dist = sphere['implicit_distance'] >>> type(dist) - + Plot these distances as a heatmap diff --git a/pyvista/core/filters/poly_data.py b/pyvista/core/filters/poly_data.py index b335892654..6c00633f6f 100644 --- a/pyvista/core/filters/poly_data.py +++ b/pyvista/core/filters/poly_data.py @@ -2945,7 +2945,7 @@ def collision( >>> mesh_b = pyvista.Cube((0.5, 0.5, 0.5)).extract_cells([0, 2, 4]) >>> collision, ncol = mesh_a.collision(mesh_b, cell_tolerance=1) >>> collision['ContactCells'][:10] - pyvista_ndarray([471, 471, 468, 468, 469, 469, 466, 466, 467, 467]) + array([471, 471, 468, 468, 469, 469, 466, 466, 467, 467]) Plot the collisions by creating a collision mask with the ``"ContactCells"`` field data. Cells with a collision are diff --git a/pyvista/core/pyvista_ndarray.py b/pyvista/core/pyvista_ndarray.py index 5e59a30d22..3bd1cbef5d 100644 --- a/pyvista/core/pyvista_ndarray.py +++ b/pyvista/core/pyvista_ndarray.py @@ -1,6 +1,5 @@ """Contains pyvista_ndarray a numpy ndarray type used in pyvista.""" from collections.abc import Iterable -from functools import wraps from typing import Union import numpy as np @@ -60,7 +59,7 @@ def __array_finalize__(self, obj): def __setitem__(self, key: Union[int, np.ndarray], value): """Implement [] set operator. - When the array is changed it triggers ``Modified()`` which updates + When the array is changed it triggers "Modified()" which updates all upstream objects, including any render windows holding the object. """ @@ -73,55 +72,4 @@ def __setitem__(self, key: Union[int, np.ndarray], value): if dataset is not None and dataset.Get(): dataset.Get().Modified() - @wraps(np.max) - def max(self, *args, **kwargs): - """Wrap numpy.max to return a single value when applicable.""" - output = super().max(*args, **kwargs) - if output.shape == (): - return output.item(0) - return output - - @wraps(np.mean) - def mean(self, *args, **kwargs): - """Wrap numpy.mean to return a single value when applicable.""" - output = super().mean(*args, **kwargs) - if output.shape == (): - return output.item(0) - return output - - @wraps(np.sum) - def sum(self, *args, **kwargs): - """Wrap numpy.sum to return a single value when applicable.""" - output = super().sum(*args, **kwargs) - if output.shape == (): - return output.item(0) - return output - - @wraps(np.min) - def min(self, *args, **kwargs): - """Wrap numpy.min to return a single value when applicable.""" - output = super().min(*args, **kwargs) - if output.shape == (): - return output.item(0) - return output - - @wraps(np.std) - def std(self, *args, **kwargs): - """Wrap numpy.std to return a single value when applicable.""" - output = super().std(*args, **kwargs) - if output.shape == (): - return output.item(0) - return output - - @wraps(np.prod) - def prod(self, *args, **kwargs): - """Wrap numpy.prod to return a single value when applicable.""" - output = super().prod(*args, **kwargs) - if output.shape == (): - return output.item(0) - return output - - # def __del__(self): - # del self.dataset - __getattr__ = _vtk.VTKArray.__getattr__ diff --git a/pyvista/plotting/mapper.py b/pyvista/plotting/mapper.py index 865f93420a..f8c054dfc8 100644 --- a/pyvista/plotting/mapper.py +++ b/pyvista/plotting/mapper.py @@ -93,6 +93,7 @@ def set_scalars( _using_labels = False if not np.issubdtype(scalars.dtype, np.number): + # raise TypeError('Non-numeric scalars are currently not supported for plotting.') # TODO: If str array, digitive and annotate cats, scalars = np.unique(scalars.astype('|S'), return_inverse=True) values = np.unique(scalars) @@ -102,11 +103,6 @@ def set_scalars( scalar_bar_args.setdefault('n_labels', 0) _using_labels = True - # Use only the real component if an array is complex - if np.issubdtype(scalars.dtype, complex): - scalars = scalars.astype(float) - title = f'{title}-real' - if rgb: show_scalar_bar = False if scalars.ndim != 2 or scalars.shape[1] < 3 or scalars.shape[1] > 4: diff --git a/pyvista/utilities/geometric_objects.py b/pyvista/utilities/geometric_objects.py index abca1cdf62..e54434e9e8 100644 --- a/pyvista/utilities/geometric_objects.py +++ b/pyvista/utilities/geometric_objects.py @@ -627,7 +627,7 @@ def Cube(center=(0.0, 0.0, 0.0), x_length=1.0, y_length=1.0, z_length=1.0, bound # add face index data for compatibility with PlatonicSolid # but make it inactive for backwards compatibility - cube.cell_data.set_array([1, 4, 0, 3, 5, 2], 'FaceIndex') + cube.cell_data.set_array([1, 4, 0, 3, 5, 2], ['FaceIndex']) # clean duplicate points if clean: @@ -1441,12 +1441,13 @@ def PlatonicSolid(kind='tetrahedron', radius=1.0, center=(0.0, 0.0, 0.0)): solid.SetSolidType(kind) solid.Update() solid = pyvista.wrap(solid.GetOutput()) - solid.scale(radius, inplace=True) - solid.points += np.asanyarray(center) - solid.center # rename and activate cell scalars cell_data = solid.get_array(0) solid.clear_data() solid.cell_data['FaceIndex'] = cell_data + # scale and translate + solid.scale(radius, inplace=True) + solid.points += np.asanyarray(center) - solid.center return solid diff --git a/pyvista/utilities/helpers.py b/pyvista/utilities/helpers.py index d776f999cd..a2d3f55dd6 100644 --- a/pyvista/utilities/helpers.py +++ b/pyvista/utilities/helpers.py @@ -215,14 +215,12 @@ def point_array(obj, name): Returns ------- - pyvista.ndarray or None - Wrapped array if available. + numpy.ndarray + Wrapped array. """ - try: - return obj.point_data.get_array(name) - except KeyError: - return + vtkarr = obj.GetPointData().GetAbstractArray(name) + return convert_array(vtkarr) def field_array(obj, name): @@ -238,14 +236,12 @@ def field_array(obj, name): Returns ------- - pyvista.ndarray + numpy.ndarray Wrapped array. """ - try: - return obj.field_data.get_array(name) - except KeyError: - return + vtkarr = obj.GetFieldData().GetAbstractArray(name) + return convert_array(vtkarr) def cell_array(obj, name): @@ -261,14 +257,12 @@ def cell_array(obj, name): Returns ------- - pyvista.ndarray + numpy.ndarray Wrapped array. """ - try: - return obj.cell_data.get_array(name) - except KeyError: - return + vtkarr = obj.GetCellData().GetAbstractArray(name) + return convert_array(vtkarr) def row_array(obj, name): diff --git a/pyvista/utilities/reader.py b/pyvista/utilities/reader.py index aaf644a970..f13f1c1b52 100644 --- a/pyvista/utilities/reader.py +++ b/pyvista/utilities/reader.py @@ -772,7 +772,7 @@ def active_time_value(self): # noqa: D102 def set_active_time_value(self, time_value): # noqa: D102 if time_value not in self.time_values: raise ValueError( - f"Not a valid time {time_value} from available time values: {self.reader_time_values}" + f"Not a valid time {time_value} from available time values: {self.time_values}" ) self.reader.UpdateTimeStep(time_value) @@ -1272,12 +1272,12 @@ class CGNSReader(BaseReader, PointCellDataSelection): Active Texture : None Active Normals : None Contains arrays : - Density float64 (2928,) - Momentum float64 (2928, 3) VECTORS - EnergyStagnationDensity float64 (2928,) - ViscosityEddy float64 (2928,) - TurbulentDistance float64 (2928,) - TurbulentSANuTilde float64 (2928,) + Density float64 (2928,) + Momentum float64 (2928, 3) VECTORS + EnergyStagnationDensity float64 (2928,) + ViscosityEddy float64 (2928,) + TurbulentDistance float64 (2928,) + TurbulentSANuTilde float64 (2928,) """ diff --git a/tests/plotting/test_plotting.py b/tests/plotting/test_plotting.py index 2c75c27f47..0f0558e427 100644 --- a/tests/plotting/test_plotting.py +++ b/tests/plotting/test_plotting.py @@ -2334,16 +2334,3 @@ def test_ruler(sphere): plotter.add_ruler([-0.6, -0.6, 0], [0.6, -0.6, 0], font_size_factor=1.2) plotter.view_xy() plotter.show(before_close_callback=verify_cache_image) - - -def test_plot_complex_value(plane): - """Test plotting complex data.""" - data = np.arange(plane.n_points, dtype=np.complex128) - data += np.linspace(0, 1, plane.n_points) * -1j - with pytest.warns(np.ComplexWarning): - plane.plot(scalars=data) - - pl = pyvista.Plotter() - with pytest.warns(np.ComplexWarning): - pl.add_mesh(plane, scalars=data, show_scalar_bar=False) - pl.show(before_close_callback=verify_cache_image) diff --git a/tests/test_datasetattributes.py b/tests/test_datasetattributes.py index f277c7a9d7..e550b45650 100644 --- a/tests/test_datasetattributes.py +++ b/tests/test_datasetattributes.py @@ -1,6 +1,5 @@ from string import ascii_letters, digits, whitespace import sys -import weakref from hypothesis import HealthCheck, given, settings from hypothesis.extra.numpy import arrays @@ -518,44 +517,3 @@ def test_active_t_coords_name(plane): with raises(AttributeError): plane.field_data.active_t_coords_name = 'arr' - - -def test_complex(plane): - """Test if complex data can be properly represented in datasetattributes.""" - name = 'my_data' - with raises(ValueError, match='Only numpy.complex128'): - plane.point_data[name] = np.empty(plane.n_points, dtype=np.complex64) - - with raises(ValueError, match='Complex data must be single dimensional'): - plane.point_data[name] = np.empty((plane.n_points, 2), dtype=np.complex128) - - data = np.random.random((plane.n_points, 2)).view(np.complex128).ravel() - plane.point_data[name] = data - assert np.allclose(plane.point_data[name], data) - - assert 'complex128' in str(plane.point_data) - - # test setter - plane.active_scalars_name = name - - # ensure that association is removed when changing datatype - assert plane.point_data[name].dtype == np.complex128 - plane.point_data[name] = plane.point_data[name].real - assert np.issubdtype(plane.point_data[name].dtype, float) - - -def test_complex_collection(plane): - name = 'my_data' - data = np.random.random((plane.n_points, 2)).view(np.complex128).ravel() - plane.point_data[name] = data - - # ensure shallow copy - data[0] += 1 - data_copy = data.copy() - assert np.allclose(plane.point_data[name], data) - - # ensure references remain - ref = weakref.ref(data) - del data - assert np.allclose(plane.point_data[name], data_copy) - assert ref is not None diff --git a/tests/test_pyvista_ndarray.py b/tests/test_pyvista_ndarray.py index e07c02d168..706c793ad0 100644 --- a/tests/test_pyvista_ndarray.py +++ b/tests/test_pyvista_ndarray.py @@ -64,12 +64,3 @@ def test_slices_are_associated_single_index(): assert points[1, 1].VTKObject == points.VTKObject assert points[1, 1].dataset.Get() == points.dataset.Get() assert points[1, 1].association == points.association - - -def test_methods_return_float(): - # ensure that methods like np.sum return float just like numpy - arr = pyvista_ndarray([1.2, 1.3]) - # breakpoint() - # arr.max() - assert isinstance(arr.max(), float) - assert isinstance(arr.sum(), float) From 997b3ce90732bbf0c86ce98347fb38d7eadaa88b Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 12 Jun 2022 01:30:30 -0600 Subject: [PATCH 26/51] remove remaining complex/float changes --- pyvista/examples/downloads.py | 4 ++-- pyvista/examples/examples.py | 4 ++-- .../plotting/image_cache/plot_complex_value.png | Bin 16436 -> 0 bytes 3 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 tests/plotting/image_cache/plot_complex_value.png diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py index b8c00da4a1..362c351a5d 100644 --- a/pyvista/examples/downloads.py +++ b/pyvista/examples/downloads.py @@ -3217,8 +3217,8 @@ def download_mars_jpg(): # pragma: no cover Active Texture : Texture Coordinates Active Normals : Normals Contains arrays : - Normals float32 (14280, 3) NORMALS - Texture Coordinates float64 (14280, 2) TCOORDS + Normals float32 (14280, 3) NORMALS + Texture Coordinates float64 (14280, 2) TCOORDS Plot with stars in the background. diff --git a/pyvista/examples/examples.py b/pyvista/examples/examples.py index 2ef8a09768..6e4d77745e 100755 --- a/pyvista/examples/examples.py +++ b/pyvista/examples/examples.py @@ -299,8 +299,8 @@ def load_sphere_vectors(): Active Texture : None Active Normals : Normals Contains arrays : - Normals float32 (842, 3) NORMALS - vectors float32 (842, 3) VECTORS + Normals float32 (842, 3) NORMALS + vectors float32 (842, 3) VECTORS """ sphere = pyvista.Sphere(radius=3.14) diff --git a/tests/plotting/image_cache/plot_complex_value.png b/tests/plotting/image_cache/plot_complex_value.png deleted file mode 100644 index 6b1123a90fc7ce7cf10893a603eff522b6f9129d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16436 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 From 421c833da6c6e1b71a913fab5c086a2198b9c932 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Tue, 21 Jun 2022 19:24:14 -0600 Subject: [PATCH 27/51] fix axes bounds fontsize --- examples/01-filter/image-fft-perlin-noise.py | 29 ++++++++++-------- examples/01-filter/image-fft.py | 24 +++++++++------ pyvista/plotting/plotting.py | 19 ++++++++++++ pyvista/plotting/renderer.py | 31 +++++++++++++------- tests/test_plotter.py | 18 ++++++++++++ 5 files changed, 89 insertions(+), 32 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 5203d9e4c4..e4484a61d1 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -52,17 +52,13 @@ # quadrant. # # Note the usage of :func:`numpy.fft.fftfreq` to get the frequencies. -# + sampled_fft = sampled.fft() freq = np.fft.fftfreq(sampled.dimensions[0], sampled.spacing[0]) +max_freq = freq.max() # only show the first quadrant -subset = sampled_fft.extract_subset((0, xdim // 4, 0, ydim // 4, 0, 0)) - -# shift the position of the uniform grid to match the frequency -subset.origin = (0, 0, 0) -spacing = np.diff(freq[:2])[0] -subset.spacing = (spacing, spacing, spacing) +subset = sampled_fft.extract_subset((0, xdim // 2, 0, ydim // 2, 0, 0)) ############################################################################### @@ -74,15 +70,18 @@ # scale by log to make the plot viewable subset['scalars'] = np.abs(subset.active_scalars.real) -warped_subset = subset.warp_by_scalar(factor=0.001) +warped_subset = subset.warp_by_scalar(factor=0.0001) pl = pv.Plotter(lighting='three lights') pl.add_mesh(warped_subset, cmap='blues', show_scalar_bar=False) pl.show_bounds( + axes_ranges=(0, max_freq, 0, max_freq, 0, max_freq), xlabel='X Frequency', ylabel='Y Frequency', zlabel='Amplitude', + show_zlabels=False, color='k', + font_size=26, ) pl.add_text('Frequency Domain of the Perlin Noise') pl.show() @@ -138,7 +137,7 @@ pl.add_text('Original Dataset') pl.subplot(0, 1) pl.add_mesh(grid.warp_by_scalar(), show_scalar_bar=False, lighting=False) -pl.add_text('Summed Low and High Passes') +pl.add_text('Sum of the Low and High Passes') pl.show() @@ -155,19 +154,23 @@ def warp_low_pass_noise(cfreq): return output.warp_by_scalar() -# initialize the plotter and plot off-screen +# Initialize the plotter and plot off-screen to save the animation as a GIF. plotter = pv.Plotter(notebook=False, off_screen=True) plotter.open_gif("low_pass.gif", fps=8) # add the initial mesh init_mesh = warp_low_pass_noise(1e-2) -plotter.add_mesh(init_mesh, show_scalar_bar=False, lighting=False) +plotter.add_mesh(init_mesh, show_scalar_bar=False, lighting=False, n_colors=128) for freq in np.logspace(-2, 1, 25): + plotter.clear() mesh = warp_low_pass_noise(freq) - plotter.add_mesh(mesh, show_scalar_bar=False, lighting=False) + plotter.add_mesh(mesh, show_scalar_bar=False, lighting=False, n_colors=128) plotter.add_text(f"Cutoff Frequency: {freq:.2f}", color="black") plotter.write_frame() - plotter.clear() + +# write the last frame a few times to "pause" the gif +for _ in range(10): + plotter.write_frame() plotter.close() diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index 5fa5c95838..d56263c62c 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -17,6 +17,8 @@ """ +import numpy as np + import pyvista as pv from pyvista import examples @@ -33,6 +35,7 @@ grey_theme.axes.show = False image.plot(theme=grey_theme, cpos='xy', text='Unprocessed Moon Landing Image') + ############################################################################### # Apply FFT to the image # ~~~~~~~~~~~~~~~~~~~~~~ @@ -45,26 +48,25 @@ fft_image = image.fft() fft_image.point_data + ############################################################################### # Plot the FFT of the image. # ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Plot the absolute value of the FFT of the image. +# # Note that we are effectively viewing the "frequency" of the data in this # image, where the four corners contain the low frequency content of the image, # and the middle is the high frequency content of the image. -# -# .. note:: -# VTK internally creates a normalized array to plot both the real and -# imaginary values from the FFT filter. To avoid having this array included -# in the dataset, we use :func:`copy() ` to create a -# temporary copy that's plotted. -fft_image.copy().plot( +fft_image.plot( + scalars=np.abs(fft_image.point_data['PNGImage']), cpos="xy", theme=grey_theme, log_scale=True, text='Moon Landing Image FFT', ) + ############################################################################### # Remove the noise from the ``fft_image`` # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -82,16 +84,18 @@ per_keep = 0.10 +# modify the fft_image data width, height, _ = fft_image.dimensions data = fft_image['PNGImage'].reshape(height, width) # note: axes flipped data[int(height * per_keep) : -int(height * per_keep)] = 0 data[:, int(width * per_keep) : -int(width * per_keep)] = 0 -fft_image.copy().plot( +fft_image.plot( + scalars=np.abs(data), cpos="xy", theme=grey_theme, log_scale=True, - text='Moon Landing Image FFT with Noise Removed ', + text='Moon Landing Image FFT with Noise Removed', ) @@ -100,5 +104,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Finally, convert the image data back to the "spacial" domain and plot it. + rfft = fft_image.rfft() +rfft['PNGImage'] = np.real(rfft['PNGImage']) rfft.plot(cpos="xy", theme=grey_theme, text='Processed Moon Landing Image') diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py index db3b2bc565..fc5ab699e2 100644 --- a/pyvista/plotting/plotting.py +++ b/pyvista/plotting/plotting.py @@ -272,6 +272,7 @@ def __init__( self.last_image = None self._has_background_layer = False self._added_scalars = [] + self._prev_active_scalars = [] # set hidden line removal based on theme if self.theme.hidden_line_removal: @@ -2380,6 +2381,15 @@ def add_mesh( # Scalars formatting ================================================== added_scalar_info = None if scalars is not None: + # track the previous active scalars + self._prev_active_scalars.append( + ( + mesh, + mesh.point_data.active_scalars_name, + mesh.cell_data.active_scalars_name, + ) + ) + show_scalar_bar, n_colors, clim, added_scalar_info = self.mapper.set_scalars( mesh, scalars, @@ -4654,6 +4664,15 @@ def remove_and_reactivate_prior_scalars(dsattr, name): remove_and_reactivate_prior_scalars(dsattr, name) self._added_scalars = [] + # reactivate prior active scalars + for mesh, point_name, cell_name in self._prev_active_scalars: + if point_name is not None: + if mesh.point_data.active_scalars_name != point_name: + mesh.point_data.active_scalars_name = point_name + if cell_name is not None: + if mesh.cell_data.active_scalars_name != cell_name: + mesh.cell_data.active_scalars_name = cell_name + def __del__(self): """Delete the plotter.""" # We have to check here if it has the closed attribute as it diff --git a/pyvista/plotting/renderer.py b/pyvista/plotting/renderer.py index 53ce5d09da..c97243a1a6 100644 --- a/pyvista/plotting/renderer.py +++ b/pyvista/plotting/renderer.py @@ -1055,6 +1055,7 @@ def show_bounds( show_ylabels=True, show_zlabels=True, bold=True, + size=10.0, font_size=None, font_family=None, color=None, @@ -1230,6 +1231,7 @@ def show_bounds( # create actor cube_axes_actor = _vtk.vtkCubeAxesActor() + cube_axes_actor.SetScreenSize(1.0) if use_2d or not np.allclose(self.scale, [1.0, 1.0, 1.0]): cube_axes_actor.SetUse2DMode(True) else: @@ -1374,16 +1376,25 @@ def show_bounds( # set font font_family = parse_font_family(font_family) - for i in range(3): - cube_axes_actor.GetTitleTextProperty(i).SetFontSize(font_size) - cube_axes_actor.GetTitleTextProperty(i).SetColor(color.float_rgb) - cube_axes_actor.GetTitleTextProperty(i).SetFontFamily(font_family) - cube_axes_actor.GetTitleTextProperty(i).SetBold(bold) - - cube_axes_actor.GetLabelTextProperty(i).SetFontSize(font_size) - cube_axes_actor.GetLabelTextProperty(i).SetColor(color.float_rgb) - cube_axes_actor.GetLabelTextProperty(i).SetFontFamily(font_family) - cube_axes_actor.GetLabelTextProperty(i).SetBold(bold) + props = [ + cube_axes_actor.GetTitleTextProperty(0), + cube_axes_actor.GetTitleTextProperty(1), + cube_axes_actor.GetTitleTextProperty(2), + cube_axes_actor.GetLabelTextProperty(0), + cube_axes_actor.GetLabelTextProperty(1), + cube_axes_actor.GetLabelTextProperty(2), + ] + + for prop in props: + prop.SetColor(color.float_rgb) + prop.SetFontFamily(font_family) + prop.SetBold(bold) + + # Note: font_size does nothing as a property, use SetScreenSize instead + # Here, we normalize relative to 16 to give the user an illusion of + # just changing "font size". 10 is used here since it's the default + # "screen size". + cube_axes_actor.SetScreenSize(font_size / 16 * 10.0) self.add_actor(cube_axes_actor, reset_camera=False, pickable=False, render=render) self.cube_axes_actor = cube_axes_actor diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 7cfa481820..e039fb3f3a 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -194,3 +194,21 @@ def test_remove_scalars_rgba(sphere): # TODO: we are not re-enabling the old scalars # assert sphere.point_data.active_scalars_name == point_data_name + + +def test_active_scalars_remain(sphere, hexbeam): + """Ensure active scalars remain active despite plotting different scalars.""" + point_data_name = 'point_data' + cell_data_name = 'cell_data' + sphere[point_data_name] = np.random.random(sphere.n_points) + hexbeam[cell_data_name] = np.random.random(hexbeam.n_cells) + assert sphere.point_data.active_scalars_name == point_data_name + assert hexbeam.cell_data.active_scalars_name == cell_data_name + + pl = pyvista.Plotter() + pl.add_mesh(sphere, scalars=range(sphere.n_points)) + pl.add_mesh(hexbeam, scalars=range(hexbeam.n_cells)) + pl.close() + + assert sphere.point_data.active_scalars_name == point_data_name + assert hexbeam.cell_data.active_scalars_name == cell_data_name From 47885591e3ed21d5ddacc942dfb527b0798ccfde Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Tue, 21 Jun 2022 21:34:26 -0600 Subject: [PATCH 28/51] allow active scalars --- pyvista/core/filters/uniform_grid.py | 12 +++++++++++- pyvista/errors.py | 4 ++++ tests/test_grid.py | 8 +++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 24617c8e0a..9f0d646b6a 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -7,6 +7,7 @@ from pyvista import _vtk, abstract_class from pyvista.core.filters import _get_output, _update_alg from pyvista.core.filters.data_set import DataSetFilters +from pyvista.errors import AmbiguousDataError, MissingDataError @abstract_class @@ -678,7 +679,16 @@ def _check_fft_scalars(self): """ # check for active scalars, otherwise risk of segfault if self.point_data.active_scalars_name is None: - raise ValueError('FFT filters require active point scalars') + possible_scalars = self.point_data.keys() + if len(possible_scalars) == 1: + self.set_active_scalars(possible_scalars[0], preference='point') + elif len(possible_scalars) > 1: + raise AmbiguousDataError( + 'There are multiple point scalars available. Set one to be active' + 'active with `point_data.active_scalars_name = `' + ) + else: + raise MissingDataError('FFT filters require point scalars.') scalars = self.point_data.active_scalars diff --git a/pyvista/errors.py b/pyvista/errors.py index 6474eb87b5..892eba596e 100644 --- a/pyvista/errors.py +++ b/pyvista/errors.py @@ -4,6 +4,10 @@ class MissingDataError(ValueError): """Exception when data is missing, e.g. no active scalars can be set.""" + def __init__(self, message='No data available.'): + """Call the base class constructor with the custom message.""" + super().__init__(message) + class AmbiguousDataError(ValueError): """Exception when data is ambiguous, e.g. multiple active scalars can be set.""" diff --git a/tests/test_grid.py b/tests/test_grid.py index 71698fc028..3843838e77 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -945,9 +945,15 @@ def test_fft_low_pass(noise_2d): name = noise_2d.active_scalars_name noise_no_scalars = noise_2d.copy() noise_no_scalars.clear_data() - with pytest.raises(ValueError, match='active point scalars'): + with pytest.raises(ValueError, match='FFT filters require'): noise_no_scalars.low_pass(1, 1, 1) + noise_too_many_scalars = noise_no_scalars.copy() + noise_too_many_scalars.point_data.set_array(np.arange(noise_2d.n_points), 'a') + noise_too_many_scalars.point_data.set_array(np.arange(noise_2d.n_points), 'b') + with pytest.raises(ValueError, match='There are multiple point scalars available'): + noise_too_many_scalars.low_pass(1, 1, 1) + with pytest.raises(ValueError, match='must be complex data'): noise_2d.low_pass(1, 1, 1) From 2b26cccfc95ac76c5c082c82258e360aa64712d7 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Tue, 21 Jun 2022 22:14:29 -0600 Subject: [PATCH 29/51] improve doc --- pyvista/core/filters/uniform_grid.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 9f0d646b6a..b9bbe8aa1f 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -544,10 +544,12 @@ def low_pass( output_scalars_name=None, progress_bar=False, ): - """Perform a low pass filter in the frequency domain. + """Perform a butterworth low pass filter in the frequency domain. - This filter only works on an image after it has been converted to - frequency domain by a :func:`UniformGridFilters.fft` filter. + This filter requires that the :class`UniformGrid` have complex point + scalars, usually generated after the :class:`UniformGrid` has been + converted to frequency domain by a :func:`UniformGridFilters.fft` + filter. A :func:`UniformGridFilters.rfft` filter can be used to convert the output back into the spatial @@ -607,10 +609,12 @@ def high_pass( output_scalars_name=None, progress_bar=False, ): - """Perform a high pass filter in the frequency domain. + """Perform a butterworth high pass filter in the frequency domain. - This filter only works on an image after it has been converted to - frequency domain by a :func:`UniformGridFilters.fft` filter. + This filter requires that the :class`UniformGrid` have complex point + scalars, usually generated after the :class:`UniformGrid` has been + converted to frequency domain by a :func:`UniformGridFilters.fft` + filter. A :func:`UniformGridFilters.rfft` filter can be used to convert the output back into the spatial From 09fc04cf899bd187710524341b703d4301568698 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Tue, 21 Jun 2022 22:27:23 -0600 Subject: [PATCH 30/51] improve docs; fix font_size scaling --- pyvista/plotting/plotting.py | 4 +++- pyvista/plotting/renderer.py | 22 ++++++++++++++-------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py index fc5ab699e2..079b5cb2ff 100644 --- a/pyvista/plotting/plotting.py +++ b/pyvista/plotting/plotting.py @@ -4662,7 +4662,6 @@ def remove_and_reactivate_prior_scalars(dsattr, name): for mesh, (name, assoc) in self._added_scalars: dsattr = mesh.point_data if assoc == 'point' else mesh.cell_data remove_and_reactivate_prior_scalars(dsattr, name) - self._added_scalars = [] # reactivate prior active scalars for mesh, point_name, cell_name in self._prev_active_scalars: @@ -4673,6 +4672,9 @@ def remove_and_reactivate_prior_scalars(dsattr, name): if mesh.cell_data.active_scalars_name != cell_name: mesh.cell_data.active_scalars_name = cell_name + self._added_scalars = [] + self._prev_active_scalars = [] + def __del__(self): """Delete the plotter.""" # We have to check here if it has the closed attribute as it diff --git a/pyvista/plotting/renderer.py b/pyvista/plotting/renderer.py index c97243a1a6..5483e2ce6e 100644 --- a/pyvista/plotting/renderer.py +++ b/pyvista/plotting/renderer.py @@ -1117,15 +1117,21 @@ def show_bounds( Bolds axis labels and numbers. Default ``True``. font_size : float, optional - Sets the size of the label font. Defaults to 16. + Sets the size of the label font. Defaults to + :attr:`pyvista.global_theme.font.size + `. font_family : str, optional Font family. Must be either ``'courier'``, ``'times'``, - or ``'arial'``. + or ``'arial'``. Defaults to :attr:`pyvista.global_theme.font.family + `. color : color_like, optional - Color of all labels and axis titles. Default white. - Either a string, rgb list, or hex color string. For + Color of all labels and axis titles. Defaults to + :attr:`pyvista.global_theme.font.color + `. + + Either a string, RGB list, or hex color string. For example: * ``color='white'`` @@ -1391,10 +1397,10 @@ def show_bounds( prop.SetBold(bold) # Note: font_size does nothing as a property, use SetScreenSize instead - # Here, we normalize relative to 16 to give the user an illusion of - # just changing "font size". 10 is used here since it's the default - # "screen size". - cube_axes_actor.SetScreenSize(font_size / 16 * 10.0) + # Here, we normalize relative to 12 to give the user an illusion of + # just changing the font size relative to a font size of 12. 10 is used + # here since it's the default "screen size". + cube_axes_actor.SetScreenSize(font_size / 12 * 10.0) self.add_actor(cube_axes_actor, reset_camera=False, pickable=False, render=render) self.cube_axes_actor = cube_axes_actor From ceec4ca2f6f905bc2e504ceb2a55095988430b99 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Wed, 22 Jun 2022 16:46:54 +0200 Subject: [PATCH 31/51] Apply suggestions from code review --- examples/01-filter/image-fft-perlin-noise.py | 5 ++--- examples/01-filter/image-fft.py | 8 ++++---- pyvista/core/filters/uniform_grid.py | 9 ++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index e4484a61d1..378e7ca9fd 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -13,7 +13,6 @@ sample :func:`pyvista.perlin_noise `, and then performing FFT of the sampled noise to show the frequency content of that noise. -n """ import numpy as np @@ -31,7 +30,7 @@ # the x direction and 5 in the y direction. Units of the frequency is # ``1/pixel``. # -# Also note that the dimensions of the image are a power of 2. This is because +# Also note that the dimensions of the image are powers of 2. This is because # the FFT is much more efficient for arrays sized as a power of 2. freq = [10, 5, 0] @@ -47,7 +46,7 @@ ############################################################################### # Perform FFT of Perlin Noise # ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Next, perform a FFT of the noise and plot the frequency content. +# Next, perform an FFT of the noise and plot the frequency content. # For the sake of simplicity, we will only plot the content in the first # quadrant. # diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index d56263c62c..fb79d93264 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -42,7 +42,7 @@ # FFT will be applied to the active scalars, ``'PNGImage'``, the default # scalars name when loading a PNG image. # -# The output from the filter is a complex array stored as the same array unless +# The output from the filter is a complex array stored by the same name unless # specified using ``output_scalars_name``. fft_image = image.fft() @@ -50,8 +50,8 @@ ############################################################################### -# Plot the FFT of the image. -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Plot the FFT of the image +# ~~~~~~~~~~~~~~~~~~~~~~~~~ # Plot the absolute value of the FFT of the image. # # Note that we are effectively viewing the "frequency" of the data in this @@ -102,7 +102,7 @@ ############################################################################### # Convert to the spatial domain using reverse FFT # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -# Finally, convert the image data back to the "spacial" domain and plot it. +# Finally, convert the image data back to the "spatial" domain and plot it. rfft = fft_image.rfft() diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index b9bbe8aa1f..ebc0a75da1 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -423,8 +423,7 @@ def fft(self, output_scalars_name=None, progress_bar=False): types, but the output is always complex128. The filter is fastest for images that have power of two sizes. - The filter is fastest for images that have power of two sizes. The - filter uses a butterfly diagram for each prime factor of the + The filter uses a butterfly diagram for each prime factor of the dimension. This makes images with prime number dimensions (i.e. 17x17) much slower to compute. Multi dimensional (i.e volumes) FFT's are decomposed so that each axis executes serially. @@ -544,7 +543,7 @@ def low_pass( output_scalars_name=None, progress_bar=False, ): - """Perform a butterworth low pass filter in the frequency domain. + """Perform a Butterworth low pass filter in the frequency domain. This filter requires that the :class`UniformGrid` have complex point scalars, usually generated after the :class:`UniformGrid` has been @@ -609,7 +608,7 @@ def high_pass( output_scalars_name=None, progress_bar=False, ): - """Perform a butterworth high pass filter in the frequency domain. + """Perform a Butterworth high pass filter in the frequency domain. This filter requires that the :class`UniformGrid` have complex point scalars, usually generated after the :class:`UniformGrid` has been @@ -666,7 +665,7 @@ def high_pass( return output def _change_fft_output_scalars(self, dataset, orig_name, out_name): - """Modify the name and dtype of the output scalars for a FFT filter.""" + """Modify the name and dtype of the output scalars for an FFT filter.""" name = orig_name if out_name is None else out_name pdata = dataset.point_data if pdata.active_scalars_name != name: From d4107b67c5df6de976beb135228202e508e666d1 Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Sat, 25 Jun 2022 06:05:47 +0900 Subject: [PATCH 32/51] Fix by black --- tests/test_plotter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 78cbecff8a..e22162376a 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -220,4 +220,3 @@ def test_no_added_with_scalar_bar(sphere): pl = pyvista.Plotter() pl.add_mesh(sphere, scalar_bar_args={"title": "some_title"}) assert sphere.n_arrays == 1 - From 758ffd94e94cd2dfc7cebfd1ce8ef5be164b6b4f Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Sat, 25 Jun 2022 06:08:56 +0900 Subject: [PATCH 33/51] Fix by black --- tests/test_plotter.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index e22162376a..031aebc3a5 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -13,7 +13,7 @@ def test_plotter_image(): plotter = pyvista.Plotter() - with pytest.raises(AttributeError, match='not yet been set up'): + with pytest.raises(AttributeError, match="not yet been set up"): plotter.image @@ -70,19 +70,23 @@ def test_pickable_actors(): def test_prepare_smooth_shading_texture(globe): """Test edge cases for smooth shading""" - mesh, scalars = _plotting.prepare_smooth_shading(globe, None, True, True, False, None) + mesh, scalars = _plotting.prepare_smooth_shading( + globe, None, True, True, False, None + ) assert scalars is None - assert 'Normals' in mesh.point_data - assert 'Texture Coordinates' in mesh.point_data + assert "Normals" in mesh.point_data + assert "Texture Coordinates" in mesh.point_data def test_prepare_smooth_shading_not_poly(hexbeam): """Test edge cases for smooth shading""" - scalars_name = 'sample_point_scalars' + scalars_name = "sample_point_scalars" scalars = hexbeam.point_data[scalars_name] - mesh, scalars = _plotting.prepare_smooth_shading(hexbeam, scalars, False, True, True, None) + mesh, scalars = _plotting.prepare_smooth_shading( + hexbeam, scalars, False, True, True, None + ) - assert 'Normals' in mesh.point_data + assert "Normals" in mesh.point_data expected_mesh = hexbeam.extract_surface().compute_normals( cell_normals=False, @@ -115,11 +119,11 @@ def test_remove_scalars_single(sphere, hexbeam): assert len(pl._added_scalars) == 2 for mesh, (name, assoc) in pl._added_scalars: - assert name == 'Data' + assert name == "Data" if mesh is sphere: - assert assoc == 'point' + assert assoc == "point" else: - assert assoc == 'cell' + assert assoc == "cell" pl.close() assert pl._added_scalars == [] @@ -132,7 +136,7 @@ def test_remove_scalars_complex(sphere): """Test plotting complex data.""" data = np.arange(sphere.n_points, dtype=np.complex128) data += np.linspace(0, 1, sphere.n_points) * -1j - point_data_name = 'data' + point_data_name = "data" sphere[point_data_name] = data pl = pyvista.Plotter() @@ -155,7 +159,7 @@ def test_remove_scalars_normalized(sphere): assert sphere.n_arrays == 0 # test scalars are removed for normalized multi-component - point_data_name = 'data' + point_data_name = "data" sphere[point_data_name] = np.random.random((sphere.n_points, 3)) pl = pyvista.Plotter() pl.add_mesh(sphere, scalars=point_data_name) @@ -165,7 +169,7 @@ def test_remove_scalars_normalized(sphere): def test_remove_scalars_component(sphere): - point_data_name = 'data' + point_data_name = "data" sphere[point_data_name] = np.random.random((sphere.n_points, 3)) pl = pyvista.Plotter() pl.add_mesh(sphere, scalars=point_data_name, component=0) @@ -181,7 +185,7 @@ def test_remove_scalars_component(sphere): def test_remove_scalars_rgba(sphere): - point_data_name = 'data' + point_data_name = "data" sphere[point_data_name] = np.random.random(sphere.n_points) pl = pyvista.Plotter() pl.add_mesh(sphere, scalars=point_data_name, opacity=point_data_name) @@ -198,8 +202,8 @@ def test_remove_scalars_rgba(sphere): def test_active_scalars_remain(sphere, hexbeam): """Ensure active scalars remain active despite plotting different scalars.""" - point_data_name = 'point_data' - cell_data_name = 'cell_data' + point_data_name = "point_data" + cell_data_name = "cell_data" sphere[point_data_name] = np.random.random(sphere.n_points) hexbeam[cell_data_name] = np.random.random(hexbeam.n_cells) assert sphere.point_data.active_scalars_name == point_data_name @@ -215,7 +219,7 @@ def test_active_scalars_remain(sphere, hexbeam): def test_no_added_with_scalar_bar(sphere): - point_data_name = 'data' + point_data_name = "data" sphere[point_data_name] = np.random.random(sphere.n_points) pl = pyvista.Plotter() pl.add_mesh(sphere, scalar_bar_args={"title": "some_title"}) From 2a5572bbe7524c1568a53c09efe8e42bd1cc3052 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 24 Jun 2022 16:23:18 -0600 Subject: [PATCH 34/51] fix formatting --- tests/test_plotter.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 031aebc3a5..8eb1c1cbc0 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -70,9 +70,7 @@ def test_pickable_actors(): def test_prepare_smooth_shading_texture(globe): """Test edge cases for smooth shading""" - mesh, scalars = _plotting.prepare_smooth_shading( - globe, None, True, True, False, None - ) + mesh, scalars = _plotting.prepare_smooth_shading(globe, None, True, True, False, None) assert scalars is None assert "Normals" in mesh.point_data assert "Texture Coordinates" in mesh.point_data @@ -82,9 +80,7 @@ def test_prepare_smooth_shading_not_poly(hexbeam): """Test edge cases for smooth shading""" scalars_name = "sample_point_scalars" scalars = hexbeam.point_data[scalars_name] - mesh, scalars = _plotting.prepare_smooth_shading( - hexbeam, scalars, False, True, True, None - ) + mesh, scalars = _plotting.prepare_smooth_shading(hexbeam, scalars, False, True, True, None) assert "Normals" in mesh.point_data From d5aabc8202be7edb738a8bf21858cfa27f327a37 Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Sat, 25 Jun 2022 07:35:08 +0900 Subject: [PATCH 35/51] Fix by pre-commit --- tests/test_plotter.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 031aebc3a5..8eb1c1cbc0 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -70,9 +70,7 @@ def test_pickable_actors(): def test_prepare_smooth_shading_texture(globe): """Test edge cases for smooth shading""" - mesh, scalars = _plotting.prepare_smooth_shading( - globe, None, True, True, False, None - ) + mesh, scalars = _plotting.prepare_smooth_shading(globe, None, True, True, False, None) assert scalars is None assert "Normals" in mesh.point_data assert "Texture Coordinates" in mesh.point_data @@ -82,9 +80,7 @@ def test_prepare_smooth_shading_not_poly(hexbeam): """Test edge cases for smooth shading""" scalars_name = "sample_point_scalars" scalars = hexbeam.point_data[scalars_name] - mesh, scalars = _plotting.prepare_smooth_shading( - hexbeam, scalars, False, True, True, None - ) + mesh, scalars = _plotting.prepare_smooth_shading(hexbeam, scalars, False, True, True, None) assert "Normals" in mesh.point_data From 6ce17e5590254f7e030298d5bcb4c42cd0e46ca4 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 24 Jun 2022 17:24:29 -0600 Subject: [PATCH 36/51] fix various issues --- examples/01-filter/image-fft-perlin-noise.py | 14 +++++--- examples/01-filter/image-fft.py | 9 +++-- pyvista/core/datasetattributes.py | 18 +++++++++- pyvista/core/filters/uniform_grid.py | 31 ++++++++++------- pyvista/errors.py | 4 +++ pyvista/plotting/plotting.py | 35 ++++---------------- pyvista/plotting/renderer.py | 2 -- tests/test_datasetattributes.py | 20 +++++++++++ tests/test_grid.py | 15 ++++++--- tests/test_plotter.py | 12 +++++++ 10 files changed, 103 insertions(+), 57 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 378e7ca9fd..69471fbc3a 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -67,14 +67,14 @@ # frequency content in the x direction and this matches the frequencies given # to :func:`pyvista.perlin_noise `. -# scale by log to make the plot viewable -subset['scalars'] = np.abs(subset.active_scalars.real) +# scale to make the plot viewable +subset['scalars'] = np.abs(subset.active_scalars) warped_subset = subset.warp_by_scalar(factor=0.0001) pl = pv.Plotter(lighting='three lights') pl.add_mesh(warped_subset, cmap='blues', show_scalar_bar=False) pl.show_bounds( - axes_ranges=(0, max_freq, 0, max_freq, 0, max_freq), + axes_ranges=(0, max_freq, 0, max_freq, 0, warped_subset.bounds[-1]), xlabel='X Frequency', ylabel='Y Frequency', zlabel='Amplitude', @@ -99,7 +99,7 @@ # As expected, we only see low frequency noise. low_pass = sampled_fft.low_pass(1.0, 1.0, 1.0).rfft() -low_pass['scalars'] = low_pass.active_scalars.real +low_pass['scalars'] = np.real(low_pass.active_scalars) warped_low_pass = low_pass.warp_by_scalar() warped_low_pass.plot(show_scalar_bar=False, text='Low Pass of the Perlin Noise', lighting=False) @@ -118,7 +118,7 @@ # frequency noise has been attenuated. high_pass = sampled_fft.high_pass(1.0, 1.0, 1.0).rfft() -high_pass['scalars'] = high_pass.active_scalars.real +high_pass['scalars'] = np.real(high_pass.active_scalars) warped_high_pass = high_pass.warp_by_scalar() warped_high_pass.plot(show_scalar_bar=False, text='High Pass of the Perlin Noise', lighting=False) @@ -131,6 +131,10 @@ grid = pv.UniformGrid(dims=sampled.dimensions, spacing=sampled.spacing) grid['scalars'] = high_pass['scalars'] + low_pass['scalars'] +print( + 'Low and High Pass identical to the original:', np.allclose(grid['scalars'], sampled['scalars']) +) + pl = pv.Plotter(shape=(1, 2)) pl.add_mesh(sampled.warp_by_scalar(), show_scalar_bar=False, lighting=False) pl.add_text('Original Dataset') diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index fb79d93264..165ba9b6cc 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -71,8 +71,7 @@ # Remove the noise from the ``fft_image`` # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Effectively, we want to remove high frequency (noisy) data from our image. -# First, let's reshape by the size of the image. Note that the image data is in -# real and imaginary axes. +# First, let's reshape by the size of the image. # # Next, perform a low pass filter by removing the middle 80% of the content of # the image. Note that the high frequency content is in the middle of the array. @@ -82,13 +81,13 @@ # :func:`pyvista.UniformGridFilters.low_pass` filter. This section is here # for demonstration purposes. -per_keep = 0.10 +ratio_to_keep = 0.10 # modify the fft_image data width, height, _ = fft_image.dimensions data = fft_image['PNGImage'].reshape(height, width) # note: axes flipped -data[int(height * per_keep) : -int(height * per_keep)] = 0 -data[:, int(width * per_keep) : -int(width * per_keep)] = 0 +data[int(height * ratio_to_keep) : -int(height * ratio_to_keep)] = 0 +data[:, int(width * ratio_to_keep) : -int(width * ratio_to_keep)] = 0 fft_image.plot( scalars=np.abs(data), diff --git a/pyvista/core/datasetattributes.py b/pyvista/core/datasetattributes.py index 67fcdeef7e..68a55a8db3 100644 --- a/pyvista/core/datasetattributes.py +++ b/pyvista/core/datasetattributes.py @@ -476,11 +476,15 @@ def active_t_coords_name(self) -> Optional[str]: @active_t_coords_name.setter def active_t_coords_name(self, name: str) -> None: + if name is None: + self.SetActiveTCoords(None) + return + self._raise_no_t_coords() dtype = self[name].dtype # only vtkDataArray subclasses can be set as active attributes if np.issubdtype(dtype, np.number) or dtype == bool: - self.SetActiveScalars(name) + self.SetActiveTCoords(name) def get_array(self, key: Union[str, int]) -> pyvista_ndarray: """Get an array in this object. @@ -1150,6 +1154,10 @@ def active_scalars_name(self) -> Optional[str]: @active_scalars_name.setter def active_scalars_name(self, name: str) -> None: + # permit setting no active scalars + if name is None: + self.SetActiveScalars(None) + return self._raise_field_data_no_scalars_vectors() dtype = self[name].dtype # only vtkDataArray subclasses can be set as active attributes @@ -1177,6 +1185,10 @@ def active_vectors_name(self) -> Optional[str]: @active_vectors_name.setter def active_vectors_name(self, name: str) -> None: + # permit setting no active + if name is None: + self.SetActiveVectors(None) + return self._raise_field_data_no_scalars_vectors() if name not in self: raise KeyError(f'DataSetAttribute does not contain "{name}"') @@ -1313,6 +1325,10 @@ def active_normals_name(self) -> Optional[str]: @active_normals_name.setter def active_normals_name(self, name: str) -> None: + # permit setting no active + if name is None: + self.SetActiveNormals(None) + return self._raise_no_normals() self.SetActiveNormals(name) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index ebc0a75da1..54886d200b 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -1,4 +1,4 @@ -"""Filters module with a class to manage filters/algorithms for uniform grid datasets.""" +"""Filters with a class to manage filters/algorithms for uniform grid datasets.""" import collections.abc import numpy as np @@ -464,7 +464,14 @@ def fft(self, output_scalars_name=None, progress_bar=False): """ # check for active scalars, otherwise risk of segfault if self.point_data.active_scalars_name is None: - raise ValueError('FFT filter requires active point scalars') + try: + pyvista.set_default_active_scalars(self) + except MissingDataError: + raise MissingDataError('FFT filter requires point scalars.') from None + + # possible only cell scalars were made active + if self.point_data.active_scalars_name is None: + raise MissingDataError('FFT filter requires point scalars.') alg = _vtk.vtkImageFFT() alg.SetInputDataObject(self) @@ -550,10 +557,10 @@ def low_pass( converted to frequency domain by a :func:`UniformGridFilters.fft` filter. - A :func:`UniformGridFilters.rfft` filter can be - used to convert the output back into the spatial - domain. This filter attenuates high frequency components. - Input and output are in doubles, with two components. + A :func:`UniformGridFilters.rfft` filter can be used to convert the + output back into the spatial domain. This filter attenuates high + frequency components. Input and output are complext arrays with + datatype ``numpy.complex128``. Parameters ---------- @@ -615,10 +622,10 @@ def high_pass( converted to frequency domain by a :func:`UniformGridFilters.fft` filter. - A :func:`UniformGridFilters.rfft` filter can be - used to convert the output back into the spatial - domain. This filter attenuates low frequency components. - Input and output are in doubles, with two components. + A :func:`UniformGridFilters.rfft` filter can be used to convert the + output back into the spatial domain. This filter attenuates low + frequency components. Input and output are complext arrays with + datatype ``numpy.complex128``. Parameters ---------- @@ -675,12 +682,12 @@ def _change_fft_output_scalars(self, dataset, orig_name, out_name): dataset._association_complex_names['POINT'].add(name) def _check_fft_scalars(self): - """Check for active scalars with two components. + """Check for complext active scalars. This is necessary for rfft, low_pass, and high_pass filters. """ - # check for active scalars, otherwise risk of segfault + # check for complex active point scalars, otherwise risk of segfault if self.point_data.active_scalars_name is None: possible_scalars = self.point_data.keys() if len(possible_scalars) == 1: diff --git a/pyvista/errors.py b/pyvista/errors.py index 892eba596e..d1ebe32ee1 100644 --- a/pyvista/errors.py +++ b/pyvista/errors.py @@ -11,3 +11,7 @@ def __init__(self, message='No data available.'): class AmbiguousDataError(ValueError): """Exception when data is ambiguous, e.g. multiple active scalars can be set.""" + + def __init__(self, message="Multiple data available."): + """Call the base class constructor with the custom message.""" + super().__init__(message) diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py index a4e6e2bda7..9e4456c1ce 100644 --- a/pyvista/plotting/plotting.py +++ b/pyvista/plotting/plotting.py @@ -7,7 +7,6 @@ import os import pathlib import platform -import re import textwrap from threading import Thread import time @@ -272,7 +271,7 @@ def __init__( self.last_image = None self._has_background_layer = False self._added_scalars = [] - self._prev_active_scalars = [] + self._prev_active_scalars = {} # set hidden line removal based on theme if self.theme.hidden_line_removal: @@ -2384,13 +2383,12 @@ def add_mesh( added_scalar_info = None if scalars is not None: # track the previous active scalars - self._prev_active_scalars.append( - ( + if mesh.memory_address not in self._prev_active_scalars: + self._prev_active_scalars[mesh.memory_address] = ( mesh, mesh.point_data.active_scalars_name, mesh.cell_data.active_scalars_name, ) - ) show_scalar_bar, n_colors, clim, added_scalar_info = self.mapper.set_scalars( mesh, @@ -4642,32 +4640,13 @@ def _remove_added_scalars(self): as active scalars. """ - - def remove_and_reactivate_prior_scalars(dsattr, name): - """Remove ``name`` and reactivate prior scalars if applicable.""" - # reactivate prior active scalars - if name.endswith('-normed'): - orig_name = name[:-7] - if orig_name in dsattr: - dsattr.active_scalars_name = orig_name - elif name.endswith('-real'): - orig_name = name[:-5] - if orig_name in dsattr: - dsattr.active_scalars_name = orig_name - elif re.findall('-[0-9]+$', name): - # component - orig_scalars = re.sub('-[0-9]+$', '', name) - if orig_scalars in dsattr: - dsattr.active_scalars_name = orig_scalars - - dsattr.pop(name, None) - + # remove the added scalars for mesh, (name, assoc) in self._added_scalars: dsattr = mesh.point_data if assoc == 'point' else mesh.cell_data - remove_and_reactivate_prior_scalars(dsattr, name) + dsattr.pop(name, None) # reactivate prior active scalars - for mesh, point_name, cell_name in self._prev_active_scalars: + for mesh, point_name, cell_name in self._prev_active_scalars.values(): if point_name is not None: if mesh.point_data.active_scalars_name != point_name: mesh.point_data.active_scalars_name = point_name @@ -4676,7 +4655,7 @@ def remove_and_reactivate_prior_scalars(dsattr, name): mesh.cell_data.active_scalars_name = cell_name self._added_scalars = [] - self._prev_active_scalars = [] + self._prev_active_scalars = {} def __del__(self): """Delete the plotter.""" diff --git a/pyvista/plotting/renderer.py b/pyvista/plotting/renderer.py index e4746504fd..0c3a7d3e1d 100644 --- a/pyvista/plotting/renderer.py +++ b/pyvista/plotting/renderer.py @@ -1055,7 +1055,6 @@ def show_bounds( show_ylabels=True, show_zlabels=True, bold=True, - size=10.0, font_size=None, font_family=None, color=None, @@ -1237,7 +1236,6 @@ def show_bounds( # create actor cube_axes_actor = _vtk.vtkCubeAxesActor() - cube_axes_actor.SetScreenSize(1.0) if use_2d or not np.allclose(self.scale, [1.0, 1.0, 1.0]): cube_axes_actor.SetUse2DMode(True) else: diff --git a/tests/test_datasetattributes.py b/tests/test_datasetattributes.py index 67e1006302..c8b809d1c1 100644 --- a/tests/test_datasetattributes.py +++ b/tests/test_datasetattributes.py @@ -128,6 +128,9 @@ def test_active_scalars_name(sphere): sphere.point_data[key] = range(sphere.n_points) assert sphere.point_data.active_scalars_name == key + sphere.point_data.active_scalars_name = None + assert sphere.point_data.active_scalars_name is None + def test_set_scalars(sphere): scalars = np.array(sphere.n_points) @@ -189,6 +192,10 @@ def test_set_vectors(hexbeam): hexbeam.point_data.set_vectors(vectors, 'my-vectors') assert np.allclose(hexbeam.point_data.active_vectors, vectors) + # check clearing + hexbeam.point_data.active_vectors_name = None + assert hexbeam.point_data.active_vectors_name is None + def test_set_invalid_vectors(hexbeam): # verify non-vector data does not become active vectors @@ -197,6 +204,16 @@ def test_set_invalid_vectors(hexbeam): hexbeam.point_data.set_vectors(not_vectors, 'my-vectors') +def test_set_tcoords_name(): + mesh = pyvista.Cube() + old_name = mesh.point_data.active_t_coords_name + assert mesh.point_data.active_t_coords_name is not None + mesh.point_data.active_t_coords_name = None + + mesh.point_data.active_t_coords_name = old_name + assert mesh.point_data.active_t_coords_name == old_name + + def test_set_bitarray(hexbeam): """Test bitarrays are properly loaded and represented in datasetattributes.""" hexbeam.clear_data() @@ -459,6 +476,9 @@ def test_normals_get(plane): plane_w_normals = plane.compute_normals() assert np.array_equal(plane_w_normals.point_data.active_normals, plane_w_normals.point_normals) + plane.point_data.active_normals_name = None + assert plane.point_data.active_normals_name is None + def test_normals_set(): plane = pyvista.Plane(i_resolution=1, j_resolution=1) diff --git a/tests/test_grid.py b/tests/test_grid.py index 5dccd54a80..268225b285 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -9,6 +9,7 @@ import pyvista from pyvista import examples from pyvista._vtk import VTK9 +from pyvista.errors import AmbiguousDataError from pyvista.plotting import system_supports_plotting from pyvista.utilities.misc import PyvistaDeprecationWarning @@ -922,7 +923,11 @@ def test_cast_uniform_to_rectilinear(): def test_fft_and_rfft(noise_2d): grid = pyvista.UniformGrid(dims=(10, 10, 1)) - with pytest.raises(ValueError, match='active point scalars'): + with pytest.raises(ValueError, match='FFT'): + grid.fft() + + grid['cell_data'] = np.arange(grid.n_cells) + with pytest.raises(ValueError, match='FFT'): grid.fft() name = noise_2d.active_scalars_name @@ -937,6 +942,8 @@ def test_fft_and_rfft(noise_2d): assert np.allclose(full_pass[name].imag, 0) output_scalars_name = 'out_scalars' + # also, disable active scalars to check if it will be automatically set + noise_2d.active_scalars_name = None noise_fft = noise_2d.fft(output_scalars_name=output_scalars_name) assert output_scalars_name in noise_fft.point_data @@ -951,7 +958,7 @@ def test_fft_low_pass(noise_2d): noise_too_many_scalars = noise_no_scalars.copy() noise_too_many_scalars.point_data.set_array(np.arange(noise_2d.n_points), 'a') noise_too_many_scalars.point_data.set_array(np.arange(noise_2d.n_points), 'b') - with pytest.raises(ValueError, match='There are multiple point scalars available'): + with pytest.raises(AmbiguousDataError, match='There are multiple point scalars available'): noise_too_many_scalars.low_pass(1, 1, 1) with pytest.raises(ValueError, match='must be complex data'): @@ -967,10 +974,10 @@ def test_fft_low_pass(noise_2d): def test_fft_high_pass(noise_2d): name = noise_2d.active_scalars_name out_zeros = noise_2d.fft().high_pass(100000, 100000, 100000) - assert np.allclose(out_zeros[name][1:], 0) + assert np.allclose(out_zeros[name], 0) out = noise_2d.fft().high_pass(10, 10, 10) - assert not np.allclose(out[name][1:], 0) + assert not np.allclose(out[name], 0) @pytest.mark.parametrize('binary', [True, False]) diff --git a/tests/test_plotter.py b/tests/test_plotter.py index 8eb1c1cbc0..24e3fc0863 100644 --- a/tests/test_plotter.py +++ b/tests/test_plotter.py @@ -220,3 +220,15 @@ def test_no_added_with_scalar_bar(sphere): pl = pyvista.Plotter() pl.add_mesh(sphere, scalar_bar_args={"title": "some_title"}) assert sphere.n_arrays == 1 + + +def test_add_multiple(sphere): + point_data_name = 'data' + sphere[point_data_name] = np.random.random(sphere.n_points) + pl = pyvista.Plotter() + pl.add_mesh(sphere) + pl.add_mesh(sphere, scalars=np.arange(sphere.n_points)) + pl.add_mesh(sphere, scalars=np.arange(sphere.n_cells)) + pl.add_mesh(sphere, scalars='data') + pl.show() + assert sphere.n_arrays == 1 From 5ab9aced7af0009f057d2479d35dc6e762f5b3b4 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 24 Jun 2022 17:27:53 -0600 Subject: [PATCH 37/51] Apply suggestions from code review Co-authored-by: Andras Deak --- examples/01-filter/image-fft-perlin-noise.py | 2 +- pyvista/core/filters/uniform_grid.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 69471fbc3a..1c6b418d03 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -165,7 +165,7 @@ def warp_low_pass_noise(cfreq): init_mesh = warp_low_pass_noise(1e-2) plotter.add_mesh(init_mesh, show_scalar_bar=False, lighting=False, n_colors=128) -for freq in np.logspace(-2, 1, 25): +for freq in np.geomspace(1e-2, 10, 25): plotter.clear() mesh = warp_low_pass_noise(freq) plotter.add_mesh(mesh, show_scalar_bar=False, lighting=False, n_colors=128) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 54886d200b..0546610403 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -640,7 +640,7 @@ def high_pass( order : int, optional The order of the cutoff curve. Given from the equation - ``1 + (cutoff/freq(i, j))**(2*order)``. + ``1/(1 + (cutoff/freq(i, j))**(2*order))``. output_scalars_name : str, optional The name of the output scalars. By default this is the same as the From 2304731084a097dce2ccf7ccc061f62adf85e5e9 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 24 Jun 2022 17:29:56 -0600 Subject: [PATCH 38/51] fix docstring --- pyvista/core/filters/uniform_grid.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 54886d200b..06bfe5e2a9 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -419,9 +419,9 @@ def image_threshold( def fft(self, output_scalars_name=None, progress_bar=False): """Apply the fast fourier transform to the active scalars. - The input can have real or complex data in any components and data - types, but the output is always complex128. The filter is fastest for - images that have power of two sizes. + The input can be real or complex data, but the output is always + ``numpy.complex128``. The filter is fastest for images that have power + of two sizes. The filter uses a butterfly diagram for each prime factor of the dimension. This makes images with prime number dimensions (i.e. 17x17) From 51d7e37af1ab16e8b2a9602465a303e715e1bc00 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Fri, 24 Jun 2022 17:37:41 -0600 Subject: [PATCH 39/51] cleanup another docstring --- pyvista/core/filters/uniform_grid.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index eaaba8350e..675e2094d7 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -485,11 +485,9 @@ def fft(self, output_scalars_name=None, progress_bar=False): def rfft(self, output_scalars_name=None, progress_bar=False): """Apply the reverse fast fourier transform (RFFT) to the active scalars. - The input can have real or complex data in any components and data - types, but the output is always complex doubles, with the first - component containing real values and the second component containing - imaginary values. The filter is fastest for images that have power of - two sizes. + The input can be real or complex data, but the output is always + ``numpy.complex128``. The filter is fastest for images that have power + of two sizes. The filter uses a butterfly diagram for each prime factor of the dimension. This makes images with prime number dimensions (i.e. 17x17) From 1eb7f81b05b93d76a1e8789cae1cd4cffabc698a Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Sun, 26 Jun 2022 00:50:18 +0200 Subject: [PATCH 40/51] Fix typos and intersphinx links in uniformgrid filters --- pyvista/core/filters/uniform_grid.py | 34 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 675e2094d7..e3acdc9d7c 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -417,16 +417,16 @@ def image_threshold( return _get_output(alg) def fft(self, output_scalars_name=None, progress_bar=False): - """Apply the fast fourier transform to the active scalars. + """Apply a fast Fourier transform (FFT) to the active scalars. The input can be real or complex data, but the output is always - ``numpy.complex128``. The filter is fastest for images that have power - of two sizes. + :attr:`numpy.complex128`. The filter is fastest for images that have + power of two sizes. The filter uses a butterfly diagram for each prime factor of the dimension. This makes images with prime number dimensions (i.e. 17x17) - much slower to compute. Multi dimensional (i.e volumes) FFT's are - decomposed so that each axis executes serially. + much slower to compute. FFTs of multidimensional meshes (i.e volumes) + are decomposed so that each axis executes serially. Parameters ---------- @@ -483,16 +483,16 @@ def fft(self, output_scalars_name=None, progress_bar=False): return output def rfft(self, output_scalars_name=None, progress_bar=False): - """Apply the reverse fast fourier transform (RFFT) to the active scalars. + """Apply a reverse fast Fourier transform (RFFT) to the active scalars. The input can be real or complex data, but the output is always - ``numpy.complex128``. The filter is fastest for images that have power + :attr:`numpy.complex128`. The filter is fastest for images that have power of two sizes. The filter uses a butterfly diagram for each prime factor of the dimension. This makes images with prime number dimensions (i.e. 17x17) - much slower to compute. Multi dimensional (i.e volumes) FFT's are - decomposed so that each axis executes serially. + much slower to compute. FFTs of multidimensional meshes (i.e volumes) + are decomposed so that each axis executes serially. Parameters ---------- @@ -550,15 +550,15 @@ def low_pass( ): """Perform a Butterworth low pass filter in the frequency domain. - This filter requires that the :class`UniformGrid` have complex point + This filter requires that the :class:`UniformGrid` have complex point scalars, usually generated after the :class:`UniformGrid` has been converted to frequency domain by a :func:`UniformGridFilters.fft` filter. A :func:`UniformGridFilters.rfft` filter can be used to convert the output back into the spatial domain. This filter attenuates high - frequency components. Input and output are complext arrays with - datatype ``numpy.complex128``. + frequency components. Input and output are complex arrays with + datatype :attr:`numpy.complex128`. Parameters ---------- @@ -615,15 +615,15 @@ def high_pass( ): """Perform a Butterworth high pass filter in the frequency domain. - This filter requires that the :class`UniformGrid` have complex point + This filter requires that the :class:`UniformGrid` have complex point scalars, usually generated after the :class:`UniformGrid` has been converted to frequency domain by a :func:`UniformGridFilters.fft` filter. A :func:`UniformGridFilters.rfft` filter can be used to convert the output back into the spatial domain. This filter attenuates low - frequency components. Input and output are complext arrays with - datatype ``numpy.complex128``. + frequency components. Input and output are complex arrays with + datatype :attr:`numpy.complex128`. Parameters ---------- @@ -680,7 +680,7 @@ def _change_fft_output_scalars(self, dataset, orig_name, out_name): dataset._association_complex_names['POINT'].add(name) def _check_fft_scalars(self): - """Check for complext active scalars. + """Check for complex active scalars. This is necessary for rfft, low_pass, and high_pass filters. @@ -692,7 +692,7 @@ def _check_fft_scalars(self): self.set_active_scalars(possible_scalars[0], preference='point') elif len(possible_scalars) > 1: raise AmbiguousDataError( - 'There are multiple point scalars available. Set one to be active' + 'There are multiple point scalars available. Set one to be ' 'active with `point_data.active_scalars_name = `' ) else: From c1ae207703408a57c912cb46537ecce162505e3c Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Sun, 26 Jun 2022 00:54:58 +0200 Subject: [PATCH 41/51] Add note about standard FFT frequency layout --- pyvista/core/filters/uniform_grid.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index e3acdc9d7c..7aefdbe6ea 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -428,6 +428,13 @@ def fft(self, output_scalars_name=None, progress_bar=False): much slower to compute. FFTs of multidimensional meshes (i.e volumes) are decomposed so that each axis executes serially. + The frequencies of the output assume standard order: along each axis + first positive frequencies are assumed from 0 to the maximum, then + negative frequencies are listed from largest absolute value to + smallest. This implies that the corners of the grid correspond to low + frequencies, while the center of the grid corresponds to high + frequencies. + Parameters ---------- output_scalars_name : str, optional @@ -494,6 +501,13 @@ def rfft(self, output_scalars_name=None, progress_bar=False): much slower to compute. FFTs of multidimensional meshes (i.e volumes) are decomposed so that each axis executes serially. + The frequencies of the input assume standard order: along each axis + first positive frequencies are assumed from 0 to the maximum, then + negative frequencies are listed from largest absolute value to + smallest. This implies that the corners of the grid correspond to low + frequencies, while the center of the grid corresponds to high + frequencies. + Parameters ---------- output_scalars_name : str, optional @@ -560,6 +574,13 @@ def low_pass( frequency components. Input and output are complex arrays with datatype :attr:`numpy.complex128`. + The frequencies of the input assume standard order: along each axis + first positive frequencies are assumed from 0 to the maximum, then + negative frequencies are listed from largest absolute value to + smallest. This implies that the corners of the grid correspond to low + frequencies, while the center of the grid corresponds to high + frequencies. + Parameters ---------- x_cutoff : double @@ -625,6 +646,13 @@ def high_pass( frequency components. Input and output are complex arrays with datatype :attr:`numpy.complex128`. + The frequencies of the input assume standard order: along each axis + first positive frequencies are assumed from 0 to the maximum, then + negative frequencies are listed from largest absolute value to + smallest. This implies that the corners of the grid correspond to low + frequencies, while the center of the grid corresponds to high + frequencies. + Parameters ---------- x_cutoff : double From e81b3c6401e9cb53459b73cdf63f446a1e0cc45d Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Sun, 26 Jun 2022 00:59:39 +0200 Subject: [PATCH 42/51] Cross-link FFT methods in See Also sections --- pyvista/core/filters/uniform_grid.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 7aefdbe6ea..78005e3979 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -449,6 +449,12 @@ def fft(self, output_scalars_name=None, progress_bar=False): pyvista.UniformGrid :class:`pyvista.UniformGrid` with applied FFT. + See Also + -------- + rfft: The reverse transform. + low_pass: Low-pass filtering of FFT output. + high_pass: High-pass filtering of FFT output. + Examples -------- Apply FFT to an example image. @@ -522,6 +528,12 @@ def rfft(self, output_scalars_name=None, progress_bar=False): pyvista.UniformGrid :class:`pyvista.UniformGrid` with the applied reverse FFT. + See Also + -------- + fft: The direct transform. + low_pass: Low-pass filtering of FFT output. + high_pass: High-pass filtering of FFT output. + Examples -------- Apply reverse FFT to an example image. @@ -608,6 +620,12 @@ def low_pass( pyvista.UniformGrid :class:`pyvista.UniformGrid` with the applied low pass filter. + See Also + -------- + fft: Direct fast Fourier transform. + rfft: Reverse fast Fourier transform. + high_pass: High-pass filtering of FFT output. + Examples -------- See :ref:`image_fft_perlin_example` for a full example using this filter. @@ -678,7 +696,13 @@ def high_pass( Returns ------- pyvista.UniformGrid - UniformGrid with the applied high pass filter. + :class:`pyvista.UniformGrid` with the applied high pass filter. + + See Also + -------- + fft: Direct fast Fourier transform. + rfft: Reverse fast Fourier transform. + low_pass: Low-pass filtering of FFT output. Examples -------- From 5db6f6e814f99e8c1baaca95ced669afdd2097d7 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Sun, 26 Jun 2022 01:15:49 +0200 Subject: [PATCH 43/51] Specify data-related error subclasses in pytest.raises --- tests/test_grid.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_grid.py b/tests/test_grid.py index 268225b285..82d80c9711 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -9,7 +9,7 @@ import pyvista from pyvista import examples from pyvista._vtk import VTK9 -from pyvista.errors import AmbiguousDataError +from pyvista.errors import AmbiguousDataError, MissingDataError from pyvista.plotting import system_supports_plotting from pyvista.utilities.misc import PyvistaDeprecationWarning @@ -923,11 +923,11 @@ def test_cast_uniform_to_rectilinear(): def test_fft_and_rfft(noise_2d): grid = pyvista.UniformGrid(dims=(10, 10, 1)) - with pytest.raises(ValueError, match='FFT'): + with pytest.raises(MissingDataError, match='FFT filter requires point scalars'): grid.fft() grid['cell_data'] = np.arange(grid.n_cells) - with pytest.raises(ValueError, match='FFT'): + with pytest.raises(MissingDataError, match='FFT filter requires point scalars'): grid.fft() name = noise_2d.active_scalars_name @@ -952,7 +952,7 @@ def test_fft_low_pass(noise_2d): name = noise_2d.active_scalars_name noise_no_scalars = noise_2d.copy() noise_no_scalars.clear_data() - with pytest.raises(ValueError, match='FFT filters require'): + with pytest.raises(MissingDataError, match='FFT filters require point scalars'): noise_no_scalars.low_pass(1, 1, 1) noise_too_many_scalars = noise_no_scalars.copy() From a473c05a987df13f869399b78d0feba76ac3b776 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Sun, 26 Jun 2022 20:48:44 +0000 Subject: [PATCH 44/51] Apply suggestions from code review --- pyvista/core/filters/uniform_grid.py | 3 +-- tests/test_datasetattributes.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 78005e3979..df2a3229d3 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -756,6 +756,5 @@ def _check_fft_scalars(self): if not meets_req: raise ValueError( 'Active scalars must be complex data for this filter, represented ' - 'either as a 2 component float array, or as an array with ' - 'dtype=numpy.complex128' + 'as an array with `dtype=numpy.complex128`.' ) diff --git a/tests/test_datasetattributes.py b/tests/test_datasetattributes.py index c8b809d1c1..e7b0835220 100644 --- a/tests/test_datasetattributes.py +++ b/tests/test_datasetattributes.py @@ -209,6 +209,7 @@ def test_set_tcoords_name(): old_name = mesh.point_data.active_t_coords_name assert mesh.point_data.active_t_coords_name is not None mesh.point_data.active_t_coords_name = None + assert mesh.point_data.active_t_coords_name is None mesh.point_data.active_t_coords_name = old_name assert mesh.point_data.active_t_coords_name == old_name From a1a2e94950a1b610850ea28eab6e6b4ce41c5713 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Mon, 27 Jun 2022 01:28:51 +0200 Subject: [PATCH 45/51] Add 'inactive scalars' RFFT test for coverage --- tests/test_grid.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_grid.py b/tests/test_grid.py index 82d80c9711..e3d72cd22f 100644 --- a/tests/test_grid.py +++ b/tests/test_grid.py @@ -947,6 +947,12 @@ def test_fft_and_rfft(noise_2d): noise_fft = noise_2d.fft(output_scalars_name=output_scalars_name) assert output_scalars_name in noise_fft.point_data + noise_fft = noise_2d.fft() + noise_fft_inactive_scalars = noise_fft.copy() + noise_fft_inactive_scalars.active_scalars_name = None + full_pass = noise_fft_inactive_scalars.rfft() + assert np.allclose(full_pass.active_scalars, noise_fft.rfft().active_scalars) + def test_fft_low_pass(noise_2d): name = noise_2d.active_scalars_name From d8766ea4737949e77fded4732563d9ebae62a1a2 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Mon, 27 Jun 2022 02:02:08 +0200 Subject: [PATCH 46/51] Add scaled version of Perlin noise FFT example animation --- examples/01-filter/image-fft-perlin-noise.py | 31 ++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 1c6b418d03..52ba9d3599 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -150,11 +150,23 @@ # Animate the variation of the cutoff frequency. -def warp_low_pass_noise(cfreq): +def warp_low_pass_noise(cfreq, scalar_ptp=abs(sampled['scalars']).ptp()): """Process the sampled FFT and warp by scalars.""" output = sampled_fft.low_pass(cfreq, cfreq, cfreq).rfft() + + # on the left: raw FFT magnitude output['scalars'] = output.active_scalars.real - return output.warp_by_scalar() + warped_raw = output.warp_by_scalar() + + # on the right: scale to fixed warped height + output_scaled = output.translate((-11, 11, 0), inplace=False) + output_scaled['scalars_warp'] = output['scalars'] / abs(output['scalars']).ptp() * scalar_ptp + warped_scaled = output_scaled.warp_by_scalar('scalars_warp') + warped_scaled.active_scalars_name = 'scalars' + # push center back to xy plane due to peaks near 0 frequency + warped_scaled.translate((0, 0, -warped_scaled.center[-1]), inplace=True) + + return warped_raw + warped_scaled # Initialize the plotter and plot off-screen to save the animation as a GIF. @@ -164,6 +176,7 @@ def warp_low_pass_noise(cfreq): # add the initial mesh init_mesh = warp_low_pass_noise(1e-2) plotter.add_mesh(init_mesh, show_scalar_bar=False, lighting=False, n_colors=128) +plotter.camera.zoom(1.3) for freq in np.geomspace(1e-2, 10, 25): plotter.clear() @@ -177,3 +190,17 @@ def warp_low_pass_noise(cfreq): plotter.write_frame() plotter.close() + + +############################################################################### +# The left mesh in the above animation warps based on the raw values of the FFT +# amplitude. This emphasizes how taking into account more and more frequencies +# as the animation progresses, we recover a gradually larger proportion of the +# full noise sample. This is why the mesh starts "flat" and grows larger as the +# frequency cutoff is increased. +# +# In contrast, the right mesh is always warped to the same visible height, +# irrespective of the cutoff frequency. This highlights how the typical +# wavelength (size of the features) of the Perlin noise decreases as the +# frequency cutoff is increased, since wavelength and frequency are inversely +# proportional. From 160e4d5b6a57ce3a35e53215040fa803b77a7594 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Mon, 27 Jun 2022 02:13:35 +0200 Subject: [PATCH 47/51] Update untested scalars doctest output --- pyvista/core/filters/uniform_grid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index df2a3229d3..f4a0710767 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -550,7 +550,7 @@ def rfft(self, output_scalars_name=None, progress_bar=False): Active Texture : None Active Normals : None Contains arrays : - PNGImage float64 (298620, 2) SCALARS + PNGImage complex128 (298620,) SCALARS See :ref:`image_fft_example` for a full example using this filter. From cff7bc6164320d70568f90e4680159f9c3525af2 Mon Sep 17 00:00:00 2001 From: Tetsuo Koyama Date: Mon, 27 Jun 2022 14:57:56 +0900 Subject: [PATCH 48/51] Apply suggestions from code review --- examples/01-filter/image-fft-perlin-noise.py | 6 ++-- examples/01-filter/image-fft.py | 2 +- pyvista/core/filters/uniform_grid.py | 30 ++++++++++---------- pyvista/examples/downloads.py | 2 +- pyvista/plotting/plotting.py | 10 +++---- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 52ba9d3599..b3625da6ca 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -27,7 +27,7 @@ # :ref:`perlin_noise_2d_example` example. # # Note that we are generating it in a flat plane and using a frequency of 10 in -# the x direction and 5 in the y direction. Units of the frequency is +# the x direction and 5 in the y direction. Units of the frequency are # ``1/pixel``. # # Also note that the dimensions of the image are powers of 2. This is because @@ -126,7 +126,7 @@ ############################################################################### # Sum Low and High Pass # ~~~~~~~~~~~~~~~~~~~~~ -# Show that the sum of the low and high passes equal the original noise. +# Show that the sum of the low and high passes equals the original noise. grid = pv.UniformGrid(dims=sampled.dimensions, spacing=sampled.spacing) grid['scalars'] = high_pass['scalars'] + low_pass['scalars'] @@ -202,5 +202,5 @@ def warp_low_pass_noise(cfreq, scalar_ptp=abs(sampled['scalars']).ptp()): # In contrast, the right mesh is always warped to the same visible height, # irrespective of the cutoff frequency. This highlights how the typical # wavelength (size of the features) of the Perlin noise decreases as the -# frequency cutoff is increased, since wavelength and frequency are inversely +# frequency cutoff is increased since wavelength and frequency are inversely # proportional. diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index 165ba9b6cc..7891b01d48 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -71,7 +71,7 @@ # Remove the noise from the ``fft_image`` # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Effectively, we want to remove high frequency (noisy) data from our image. -# First, let's reshape by the size of the image. +# First, let's reshape the size of the image. # # Next, perform a low pass filter by removing the middle 80% of the content of # the image. Note that the high frequency content is in the middle of the array. diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index f4a0710767..1de3516116 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -421,7 +421,7 @@ def fft(self, output_scalars_name=None, progress_bar=False): The input can be real or complex data, but the output is always :attr:`numpy.complex128`. The filter is fastest for images that have - power of two sizes. + the power of two sizes. The filter uses a butterfly diagram for each prime factor of the dimension. This makes images with prime number dimensions (i.e. 17x17) @@ -430,7 +430,7 @@ def fft(self, output_scalars_name=None, progress_bar=False): The frequencies of the output assume standard order: along each axis first positive frequencies are assumed from 0 to the maximum, then - negative frequencies are listed from largest absolute value to + negative frequencies are listed from the largest absolute value to smallest. This implies that the corners of the grid correspond to low frequencies, while the center of the grid corresponds to high frequencies. @@ -438,7 +438,7 @@ def fft(self, output_scalars_name=None, progress_bar=False): Parameters ---------- output_scalars_name : str, optional - The name of the output scalars. By default this is the same as the + The name of the output scalars. By default, this is the same as the active scalars of the dataset. progress_bar : bool, optional @@ -499,7 +499,7 @@ def rfft(self, output_scalars_name=None, progress_bar=False): """Apply a reverse fast Fourier transform (RFFT) to the active scalars. The input can be real or complex data, but the output is always - :attr:`numpy.complex128`. The filter is fastest for images that have power + :attr:`numpy.complex128`. The filter is fastest for images that have the power of two sizes. The filter uses a butterfly diagram for each prime factor of the @@ -509,7 +509,7 @@ def rfft(self, output_scalars_name=None, progress_bar=False): The frequencies of the input assume standard order: along each axis first positive frequencies are assumed from 0 to the maximum, then - negative frequencies are listed from largest absolute value to + negative frequencies are listed from the largest absolute value to smallest. This implies that the corners of the grid correspond to low frequencies, while the center of the grid corresponds to high frequencies. @@ -517,7 +517,7 @@ def rfft(self, output_scalars_name=None, progress_bar=False): Parameters ---------- output_scalars_name : str, optional - The name of the output scalars. By default this is the same as the + The name of the output scalars. By default, this is the same as the active scalars of the dataset. progress_bar : bool, optional @@ -576,9 +576,9 @@ def low_pass( ): """Perform a Butterworth low pass filter in the frequency domain. - This filter requires that the :class:`UniformGrid` have complex point + This filter requires that the :class:`UniformGrid` have a complex point scalars, usually generated after the :class:`UniformGrid` has been - converted to frequency domain by a :func:`UniformGridFilters.fft` + converted to the frequency domain by a :func:`UniformGridFilters.fft` filter. A :func:`UniformGridFilters.rfft` filter can be used to convert the @@ -588,7 +588,7 @@ def low_pass( The frequencies of the input assume standard order: along each axis first positive frequencies are assumed from 0 to the maximum, then - negative frequencies are listed from largest absolute value to + negative frequencies are listed from the largest absolute value to smallest. This implies that the corners of the grid correspond to low frequencies, while the center of the grid corresponds to high frequencies. @@ -609,7 +609,7 @@ def low_pass( ``1 + (cutoff/freq(i, j))**(2*order)``. output_scalars_name : str, optional - The name of the output scalars. By default this is the same as the + The name of the output scalars. By default, this is the same as the active scalars of the dataset. progress_bar : bool, optional @@ -654,9 +654,9 @@ def high_pass( ): """Perform a Butterworth high pass filter in the frequency domain. - This filter requires that the :class:`UniformGrid` have complex point + This filter requires that the :class:`UniformGrid` have a complex point scalars, usually generated after the :class:`UniformGrid` has been - converted to frequency domain by a :func:`UniformGridFilters.fft` + converted to the frequency domain by a :func:`UniformGridFilters.fft` filter. A :func:`UniformGridFilters.rfft` filter can be used to convert the @@ -666,7 +666,7 @@ def high_pass( The frequencies of the input assume standard order: along each axis first positive frequencies are assumed from 0 to the maximum, then - negative frequencies are listed from largest absolute value to + negative frequencies are listed from the largest absolute value to smallest. This implies that the corners of the grid correspond to low frequencies, while the center of the grid corresponds to high frequencies. @@ -687,7 +687,7 @@ def high_pass( ``1/(1 + (cutoff/freq(i, j))**(2*order))``. output_scalars_name : str, optional - The name of the output scalars. By default this is the same as the + The name of the output scalars. By default, this is the same as the active scalars of the dataset. progress_bar : bool, optional @@ -737,7 +737,7 @@ def _check_fft_scalars(self): This is necessary for rfft, low_pass, and high_pass filters. """ - # check for complex active point scalars, otherwise risk of segfault + # check for complex active point scalars, otherwise the risk of segfault if self.point_data.active_scalars_name is None: possible_scalars = self.point_data.keys() if len(possible_scalars) == 1: diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py index 968670de22..1ed81c8378 100644 --- a/pyvista/examples/downloads.py +++ b/pyvista/examples/downloads.py @@ -4091,7 +4091,7 @@ def download_cells_nd(load=True): # pragma: no cover def download_moonlanding_image(load=True): # pragma: no cover - """Download the moonlanding image. + """Download the moon landing image. This is a noisy image originally obtained from `Scipy Lecture Notes `_ and can be used to demonstrate a diff --git a/pyvista/plotting/plotting.py b/pyvista/plotting/plotting.py index 9e4456c1ce..48139a206d 100644 --- a/pyvista/plotting/plotting.py +++ b/pyvista/plotting/plotting.py @@ -4647,12 +4647,10 @@ def _remove_added_scalars(self): # reactivate prior active scalars for mesh, point_name, cell_name in self._prev_active_scalars.values(): - if point_name is not None: - if mesh.point_data.active_scalars_name != point_name: - mesh.point_data.active_scalars_name = point_name - if cell_name is not None: - if mesh.cell_data.active_scalars_name != cell_name: - mesh.cell_data.active_scalars_name = cell_name + if point_name is not None and mesh.point_data.active_scalars_name != point_name: + mesh.point_data.active_scalars_name = point_name + if cell_name is not None and mesh.cell_data.active_scalars_name != cell_name: + mesh.cell_data.active_scalars_name = cell_name self._added_scalars = [] self._prev_active_scalars = {} From 8ca30df518e40adfcde51438bf82374fc91d4e52 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Mon, 27 Jun 2022 10:45:06 +0200 Subject: [PATCH 49/51] Fix Perlin FFT animation scaling --- examples/01-filter/image-fft-perlin-noise.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index b3625da6ca..02e766e692 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -150,7 +150,7 @@ # Animate the variation of the cutoff frequency. -def warp_low_pass_noise(cfreq, scalar_ptp=abs(sampled['scalars']).ptp()): +def warp_low_pass_noise(cfreq, scalar_ptp=sampled['scalars'].ptp()): """Process the sampled FFT and warp by scalars.""" output = sampled_fft.low_pass(cfreq, cfreq, cfreq).rfft() @@ -160,7 +160,7 @@ def warp_low_pass_noise(cfreq, scalar_ptp=abs(sampled['scalars']).ptp()): # on the right: scale to fixed warped height output_scaled = output.translate((-11, 11, 0), inplace=False) - output_scaled['scalars_warp'] = output['scalars'] / abs(output['scalars']).ptp() * scalar_ptp + output_scaled['scalars_warp'] = output['scalars'] / output['scalars'].ptp() * scalar_ptp warped_scaled = output_scaled.warp_by_scalar('scalars_warp') warped_scaled.active_scalars_name = 'scalars' # push center back to xy plane due to peaks near 0 frequency From 01a102995f16e93fec33086c3da509114cfb3069 Mon Sep 17 00:00:00 2001 From: Andras Deak Date: Mon, 27 Jun 2022 12:38:07 +0200 Subject: [PATCH 50/51] Update pyvista/examples/downloads.py --- pyvista/examples/downloads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyvista/examples/downloads.py b/pyvista/examples/downloads.py index 1ed81c8378..a6b2e677fc 100644 --- a/pyvista/examples/downloads.py +++ b/pyvista/examples/downloads.py @@ -4091,7 +4091,7 @@ def download_cells_nd(load=True): # pragma: no cover def download_moonlanding_image(load=True): # pragma: no cover - """Download the moon landing image. + """Download the Moon landing image. This is a noisy image originally obtained from `Scipy Lecture Notes `_ and can be used to demonstrate a From 6f0e62fdf8a51ba5f2cd1a15ff1dce235c0e4ca9 Mon Sep 17 00:00:00 2001 From: Alex Kaszynski Date: Mon, 27 Jun 2022 13:48:49 +0000 Subject: [PATCH 51/51] Apply suggestions from code review Co-authored-by: Andras Deak --- examples/01-filter/image-fft-perlin-noise.py | 2 +- examples/01-filter/image-fft.py | 2 +- pyvista/core/filters/uniform_grid.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/01-filter/image-fft-perlin-noise.py b/examples/01-filter/image-fft-perlin-noise.py index 02e766e692..b83a0799db 100644 --- a/examples/01-filter/image-fft-perlin-noise.py +++ b/examples/01-filter/image-fft-perlin-noise.py @@ -27,7 +27,7 @@ # :ref:`perlin_noise_2d_example` example. # # Note that we are generating it in a flat plane and using a frequency of 10 in -# the x direction and 5 in the y direction. Units of the frequency are +# the x direction and 5 in the y direction. The unit of frequency is # ``1/pixel``. # # Also note that the dimensions of the image are powers of 2. This is because diff --git a/examples/01-filter/image-fft.py b/examples/01-filter/image-fft.py index 7891b01d48..165ba9b6cc 100644 --- a/examples/01-filter/image-fft.py +++ b/examples/01-filter/image-fft.py @@ -71,7 +71,7 @@ # Remove the noise from the ``fft_image`` # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Effectively, we want to remove high frequency (noisy) data from our image. -# First, let's reshape the size of the image. +# First, let's reshape by the size of the image. # # Next, perform a low pass filter by removing the middle 80% of the content of # the image. Note that the high frequency content is in the middle of the array. diff --git a/pyvista/core/filters/uniform_grid.py b/pyvista/core/filters/uniform_grid.py index 1de3516116..31c6722b25 100644 --- a/pyvista/core/filters/uniform_grid.py +++ b/pyvista/core/filters/uniform_grid.py @@ -421,7 +421,7 @@ def fft(self, output_scalars_name=None, progress_bar=False): The input can be real or complex data, but the output is always :attr:`numpy.complex128`. The filter is fastest for images that have - the power of two sizes. + power of two sizes. The filter uses a butterfly diagram for each prime factor of the dimension. This makes images with prime number dimensions (i.e. 17x17) @@ -499,7 +499,7 @@ def rfft(self, output_scalars_name=None, progress_bar=False): """Apply a reverse fast Fourier transform (RFFT) to the active scalars. The input can be real or complex data, but the output is always - :attr:`numpy.complex128`. The filter is fastest for images that have the power + :attr:`numpy.complex128`. The filter is fastest for images that have power of two sizes. The filter uses a butterfly diagram for each prime factor of the