This commit is contained in:
cjw
2026-02-12 23:22:11 +08:00
parent 7b09eb3d89
commit 89660bba4e
5988 changed files with 2517516 additions and 0 deletions
@@ -0,0 +1,130 @@
"""PyVista package for 3D plotting and mesh analysis."""
from __future__ import annotations
import os
import sys
from typing import TYPE_CHECKING
from typing import Literal
import warnings
from pyvista._plot import plot as plot
from pyvista._version import __version__ as __version__
from pyvista._version import version_info as version_info
from pyvista.core import *
from pyvista.core import _validation as _validation
from pyvista.core._typing_core._dataset_types import _DataObjectType as _DataObjectType
from pyvista.core._typing_core._dataset_types import (
_DataSetOrMultiBlockType as _DataSetOrMultiBlockType,
)
from pyvista.core._typing_core._dataset_types import _DataSetType as _DataSetType
from pyvista.core._typing_core._dataset_types import _GridType as _GridType
from pyvista.core._typing_core._dataset_types import _PointGridType as _PointGridType
from pyvista.core._typing_core._dataset_types import _PointSetType as _PointSetType
from pyvista.core._vtk_core import vtk_version_info as vtk_version_info
from pyvista.core.cell import _get_vtk_id_type
from pyvista.core.utilities.observers import send_errors_to_logging
from pyvista.core.wrappers import _wrappers as _wrappers
from pyvista.jupyter import set_jupyter_backend as set_jupyter_backend
from pyvista.report import GPUInfo as GPUInfo
from pyvista.report import Report as Report
from pyvista.report import get_gpu_info as get_gpu_info
if TYPE_CHECKING:
import numpy as np
# get the int type from vtk
ID_TYPE: type[np.int32 | np.int64] = _get_vtk_id_type()
# determine if using at least vtk 9.0.0
if vtk_version_info.major < 9: # pragma: no cover
from pyvista.core.errors import VTKVersionError
msg = 'VTK version must be 9.0.0 or greater.'
raise VTKVersionError(msg)
# catch annoying numpy/vtk future warning:
warnings.simplefilter(action='ignore', category=FutureWarning)
# A simple flag to set when generating the documentation
OFF_SCREEN = os.environ.get('PYVISTA_OFF_SCREEN', 'false').lower() == 'true'
# flag for when building the sphinx_gallery
BUILDING_GALLERY = os.environ.get('PYVISTA_BUILDING_GALLERY', 'false').lower() == 'true'
# A threshold for the max cells to compute a volume for when repr-ing
REPR_VOLUME_MAX_CELLS = 1e6
# Set where figures are saved
FIGURE_PATH = os.environ.get('PYVISTA_FIGURE_PATH', None)
ON_SCREENSHOT = os.environ.get('PYVISTA_ON_SCREENSHOT', 'false').lower() == 'true'
# Send VTK messages to the logging module:
send_errors_to_logging()
# theme to use by default for the plot directive
PLOT_DIRECTIVE_THEME = None
# Set a parameter to control default print format for floats outside
# of the plotter
FLOAT_FORMAT = '{:.3e}'
# Serialization format to be used when pickling `DataObject`
PICKLE_FORMAT: Literal['vtk', 'xml', 'legacy'] = 'vtk' if vtk_version_info >= (9, 3) else 'xml'
# Name used for unnamed scalars
DEFAULT_SCALARS_NAME = 'Data'
MAX_N_COLOR_BARS = 10
_VTK_SNAKE_CASE_STATE: Literal['allow', 'warning', 'error'] = 'error'
# Import all modules for type checkers and linters
if TYPE_CHECKING:
from pyvista import demos as demos
from pyvista import examples as examples
from pyvista import ext as ext
from pyvista import trame as trame
from pyvista import utilities as utilities
from pyvista.plotting import *
# Lazily import/access the plotting module
def __getattr__(name):
"""Fetch an attribute ``name`` from ``globals()`` or the ``pyvista.plotting`` module.
This override is implemented to prevent importing all of the plotting module
and GL-dependent VTK modules when importing PyVista.
Raises
------
AttributeError
If the attribute is not found.
"""
import importlib # noqa: PLC0415
import inspect # noqa: PLC0415
allow = {
'demos',
'examples',
'ext',
'trame',
'utilities',
}
if name in allow:
return importlib.import_module(f'pyvista.{name}')
# avoid recursive import
if 'pyvista.plotting' not in sys.modules:
import pyvista.plotting # noqa: F401, PLC0415
try:
feature = inspect.getattr_static(sys.modules['pyvista.plotting'], name)
except AttributeError:
msg = f"module 'pyvista' has no attribute '{name}'"
raise AttributeError(msg) from None
return feature
@@ -0,0 +1,251 @@
from __future__ import annotations
from functools import wraps
import inspect
from inspect import Parameter
from inspect import Signature
import os
from pathlib import Path
from typing import Callable
from typing import TypeVar
from typing import overload
import warnings
from typing_extensions import ParamSpec
from pyvista._version import version_info
_MAX_POSITIONAL_ARGS = 3 # Should match value in pyproject.toml
P = ParamSpec('P')
T = TypeVar('T')
@overload
def _deprecate_positional_args(
func: Callable[P, T],
*,
version: tuple[int, int] = ...,
allowed: list[str] | None = ...,
n_allowed: int = ...,
) -> Callable[P, T]: ...
@overload
def _deprecate_positional_args(
*, version: tuple[int, int] = ..., allowed: list[str] | None = ..., n_allowed: int = ...
) -> Callable[[Callable[P, T]], Callable[P, T]]: ...
def _deprecate_positional_args(
func: Callable[..., T] | None = None,
*,
version: tuple[int, int] = (0, 50),
allowed: list[str] | None = None,
n_allowed: int | None = None,
) -> Callable[..., T] | Callable[[Callable[P, T]], Callable[P, T]]:
"""Use a decorator to deprecate positional arguments.
Parameters
----------
func : callable, default=None
Function to check arguments on.
version : tuple[int, int], default: (0, 50)
The version (major, minor) when positional arguments will result in RuntimeError.
allowed : list[str], optional
List of argument names which are allowed to be positional. This value is limited
based on rule PLR0917.
n_allowed : int, optional
Override the number of allowed positional arguments to this value.
"""
def _inner_deprecate_positional_args(f: Callable[P, T]) -> Callable[P, T]:
def qualified_name() -> str:
return f.__qualname__ if hasattr(f, '__qualname__') else f.__name__
decorator_name = _deprecate_positional_args.__name__
sig = inspect.signature(f)
param_names = list(sig.parameters)
# Validate n_allowed itself
if n_allowed:
if n_allowed <= _MAX_POSITIONAL_ARGS:
msg = (
f'In decorator {decorator_name!r} for function {qualified_name()!r}:\n'
f'`n_allowed` must be greater than {_MAX_POSITIONAL_ARGS} for it to be useful.'
)
raise ValueError(msg)
n_allowed_ = n_allowed
else:
n_allowed_ = _MAX_POSITIONAL_ARGS
if allowed is not None:
# Validate input type
if not isinstance(allowed, list):
msg = ( # type: ignore[unreachable]
f'In decorator {decorator_name!r} for function {qualified_name()!r}:\n'
f'Allowed arguments must be a list, got {type(allowed)}.'
)
raise TypeError(msg)
# Validate number of allowed args
if len(allowed) > n_allowed_:
msg = (
f'In decorator {decorator_name!r} for function {qualified_name()!r}:\n'
f'A maximum of {n_allowed_} positional arguments are allowed.\n'
f'Got {len(allowed)}: {allowed}'
)
raise ValueError(msg)
# Validate allowed against actual parameter names
for name in allowed:
if name not in param_names:
msg = (
f'Allowed positional argument {name!r} in decorator '
f'{decorator_name!r}\n'
f'is not a parameter of function {qualified_name()!r}.'
)
raise ValueError(msg)
# Check that allowed args appears in the same order as in the signature
sig_allowed = [name for name in param_names if name in allowed]
if sig_allowed != allowed:
msg = (
f'The `allowed` list {allowed} in decorator {decorator_name!r} is not in the\n'
f'same order as the parameters in {qualified_name()!r}.\n'
f'Expected order: {sig_allowed}.'
)
raise ValueError(msg)
# Check that allowed args are not already kwonly
for name in allowed:
if sig.parameters[name].kind == Parameter.KEYWORD_ONLY:
msg = (
f'Parameter {name!r} in decorator {decorator_name!r} is already '
f'keyword-only\nand should be removed from the allowed list.'
)
raise ValueError(msg)
# Check if the decorator is even needed at all
n_positional = 0
for name in param_names:
if name not in ['cls', 'self'] and sig.parameters[name].kind in [
Parameter.POSITIONAL_ONLY,
Parameter.POSITIONAL_OR_KEYWORD,
]:
n_positional += 1
actual_n_allowed = len(allowed) if allowed else 0
if n_positional <= actual_n_allowed:
msg = (
f'Function {qualified_name()!r} has {actual_n_allowed} positional arguments, '
f'which is less than or equal to the\nmaximum number of allowed positional '
f'arguments ({n_allowed_}).\nThis decorator is not necessary and can be removed.'
)
raise RuntimeError(msg)
# Raise error post-deprecation
if version_info >= version:
# Construct expected positional args and signature
new_parameters = []
max_args_to_print = actual_n_allowed + 2
cls_or_self = 'cls' in param_names or 'self' in param_names
max_args_to_print = (max_args_to_print + 1) if cls_or_self else max_args_to_print
has_too_many_to_print = False
for i, name in enumerate(param_names):
if i > max_args_to_print:
has_too_many_to_print = True
break
if name in ['cls', 'self', *(allowed if allowed else [])]:
current_kind = sig.parameters[name].kind
new_kind = (
current_kind
if current_kind != Parameter.KEYWORD_ONLY
else Parameter.KEYWORD_ONLY
)
new_parameters.append(Parameter(name, kind=new_kind))
else:
new_parameters.append(Parameter(name, kind=Parameter.KEYWORD_ONLY))
signature_string = f'{qualified_name()}{Signature(new_parameters)}'
if has_too_many_to_print:
# Replace ending bracket with ellipses
signature_string = f'{signature_string[:-1]}, ...)'
# Get source file and line number
file = Path(
os.path.relpath(inspect.getfile(f), start=os.getcwd()) # noqa: PTH109 # https://github.com/pyvista/pyvista/pull/7732
).as_posix()
lineno = inspect.getsourcelines(f)[1]
location = f'{file}:{lineno}'
msg = (
f'Positional arguments are no longer allowed in {qualified_name()!r}.\n'
f'Update the function signature at:\n'
f'{location} to enforce keyword-only args:\n'
f' {signature_string}\n'
f'and remove the {decorator_name!r} decorator.'
)
raise RuntimeError(msg)
@wraps(f)
def inner_f(*args: P.args, **kwargs: P.kwargs) -> T:
passed_positional_names = param_names[: len(args)]
# Exclude allowed ones
if allowed:
offending_args = [name for name in passed_positional_names if name not in allowed]
else:
offending_args = passed_positional_names
if 'self' in offending_args:
offending_args.remove('self')
if 'cls' in offending_args:
offending_args.remove('cls')
if offending_args:
# Craft a message to print a warning or raise an error
if len(offending_args) == 1:
a = ' a '
s = ''
this = 'this'
else:
a = ' '
s = 's'
this = 'these'
if version_info < version:
# Print warning
version_str = '.'.join(map(str, version))
arg_list = ', '.join(f'{a!r}' for a in offending_args)
stack_level = 3
def call_site() -> str:
# Get location where the function is called
frame = inspect.stack()[stack_level]
file = Path(
os.path.relpath(frame.filename, start=os.getcwd()) # noqa: PTH109 # https://github.com/pyvista/pyvista/pull/7732
).as_posix()
return f'{file}:{frame.lineno}'
def warn_positional_args() -> None:
from pyvista.core.errors import PyVistaDeprecationWarning # noqa: PLC0415
msg = (
f'\n{call_site()}: '
f'Argument{s} {arg_list} must be passed as{a}keyword argument{s} '
f'to function {qualified_name()!r}.\n'
f'From version {version_str}, passing {this} as{a}positional '
f'argument{s} will result in a TypeError.'
)
warnings.warn(msg, PyVistaDeprecationWarning, stacklevel=stack_level)
warn_positional_args()
return f(*args, **kwargs)
return inner_f
if func is not None:
return _inner_deprecate_positional_args(func)
return _inner_deprecate_positional_args
@@ -0,0 +1,324 @@
"""PyVista's famous ``plot()`` helper method.
This method is placed at the top-level to allow us to easily bind
the method to all of the core datatypes before importing the
``pyvista.plotting`` module and libGL dependent VTK modules.
This is necessary for future versions of PyVista that will fully
decouple the ``core`` and ``plotting`` APIs.
"""
from __future__ import annotations
from pathlib import Path
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
@_deprecate_positional_args(allowed=['var_item'])
def plot( # noqa: PLR0917
var_item,
off_screen=None,
full_screen=None,
screenshot=None,
interactive=True, # noqa: FBT002
cpos=None,
window_size=None,
show_bounds=False, # noqa: FBT002
show_axes=None,
notebook=None,
background=None,
text='',
return_img=False, # noqa: FBT002
eye_dome_lighting=False, # noqa: FBT002
volume=False, # noqa: FBT002
parallel_projection=False, # noqa: FBT002
jupyter_backend=None,
return_viewer=False, # noqa: FBT002
return_cpos=False, # noqa: FBT002
jupyter_kwargs=None,
theme=None,
anti_aliasing=None,
zoom=None,
border=False, # noqa: FBT002
border_color='k',
border_width=2.0,
ssao=False, # noqa: FBT002
**kwargs,
):
"""Plot a PyVista, numpy, or vtk object.
Parameters
----------
var_item : pyvista.DataSet
See :func:`Plotter.add_mesh <pyvista.Plotter.add_mesh>` for all
supported types.
off_screen : bool, optional
Plots off screen when ``True``. Helpful for saving
screenshots without a window popping up. Defaults to the
global setting ``pyvista.OFF_SCREEN``.
full_screen : bool, default: :attr:`pyvista.plotting.themes.Theme.full_screen`
Opens window in full screen. When enabled, ignores
``window_size``.
screenshot : str or bool, optional
Saves screenshot to file when enabled. See:
:func:`Plotter.screenshot() <pyvista.Plotter.screenshot>`.
Default ``False``.
When ``True``, takes screenshot and returns ``numpy`` array of
image.
interactive : bool, default: :attr:`pyvista.plotting.themes.Theme.interactive`
Allows user to pan and move figure.
cpos : list, optional
List of camera position, focal point, and view up.
window_size : sequence, default: :attr:`pyvista.plotting.themes.Theme.window_size`
Window size in pixels.
show_bounds : bool, default: False
Shows mesh bounds when ``True``.
show_axes : bool, default: :attr:`pyvista.plotting.themes._AxesConfig.show`
Shows a vtk axes widget.
notebook : bool, default: :attr:`pyvista.plotting.themes.Theme.notebook`
When ``True``, the resulting plot is placed inline a jupyter
notebook. Assumes a jupyter console is active.
background : ColorLike, default: :attr:`pyvista.plotting.themes.Theme.background`
Color of the background.
text : str, optional
Adds text at the bottom of the plot.
return_img : bool, default: False
Returns numpy array of the last image rendered.
eye_dome_lighting : bool, optional
Enables eye dome lighting.
volume : bool, default: False
Use the :func:`Plotter.add_volume()
<pyvista.Plotter.add_volume>` method for volume rendering.
parallel_projection : bool, default: False
Enable parallel projection.
jupyter_backend : str, default: :attr:`pyvista.plotting.themes.Theme.jupyter_backend`
Jupyter notebook plotting backend to use. One of the
following:
* ``'none'`` : Do not display in the notebook.
* ``'static'`` : Display a static figure.
* ``'trame'`` : Display using ``trame``.
This can also be set globally with
:func:`pyvista.set_jupyter_backend`.
return_viewer : bool, default: False
Return the jupyterlab viewer, scene, or display object
when plotting with jupyter notebook.
return_cpos : bool, default: False
Return the last camera position from the render window
when enabled. Defaults to value in theme settings.
jupyter_kwargs : dict, optional
Keyword arguments for the Jupyter notebook plotting backend.
See :ref:`customize_trame_toolbar_example` for an example
using this keyword.
theme : pyvista.plotting.themes.Theme, optional
Plot-specific theme.
anti_aliasing : str | bool, default: :attr:`pyvista.plotting.themes.Theme.anti_aliasing`
Enable or disable anti-aliasing. If ``True``, uses ``"msaa"``. If False,
disables anti_aliasing. If a string, should be either ``"fxaa"`` or
``"ssaa"``.
zoom : float, str, optional
Camera zoom. Either ``'tight'`` or a float. A value greater than 1
is a zoom-in, a value less than 1 is a zoom-out. Must be greater
than 0.
border : bool, default: False
Draw a border around each render window.
border_color : ColorLike, default: "k"
Either a string, rgb list, or hex color string. For example:
* ``color='white'``
* ``color='w'``
* ``color=[1.0, 1.0, 1.0]``
* ``color='#FFFFFF'``
border_width : float, default: 2.0
Width of the border in pixels when enabled.
ssao : bool, optional
Enable surface space ambient occlusion (SSAO). See
:func:`Plotter.enable_ssao` for more details.
**kwargs : dict, optional
See :func:`pyvista.Plotter.add_mesh` for additional options.
Returns
-------
cpos : list
List of camera position, focal point, and view up.
Returned only when ``return_cpos=True`` or set in the
default global or plot theme. Not returned when in a
jupyter notebook and ``return_viewer=True``.
image : np.ndarray
Numpy array of the last image when either ``return_img=True``
or ``screenshot=True`` is set. Not returned when in a
jupyter notebook with ``return_viewer=True``. Optionally
contains alpha values. Sized:
* [Window height x Window width x 3] if the theme sets
``transparent_background=False``.
* [Window height x Window width x 4] if the theme sets
``transparent_background=True``.
widget : ipywidgets.Widget
IPython widget when ``return_viewer=True``.
Examples
--------
Plot a simple sphere while showing its edges.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.plot(show_edges=True)
Plot a volume mesh. Color by distance from the center of the
ImageData. Note ``volume=True`` is passed.
>>> import numpy as np
>>> grid = pv.ImageData(dimensions=(32, 32, 32), spacing=(0.5, 0.5, 0.5))
>>> grid['data'] = np.linalg.norm(grid.center - grid.points, axis=1)
>>> grid['data'] = np.abs(grid['data'] - grid['data'].max()) ** 3
>>> grid.plot(volume=True)
"""
if jupyter_kwargs is None:
jupyter_kwargs = {}
# undocumented kwarg used within pytest to run a function before closing
before_close_callback = kwargs.pop('before_close_callback', None)
# pop from kwargs here to avoid including them in add_mesh or add_volume
eye_dome_lighting = kwargs.pop('edl', eye_dome_lighting)
show_grid = kwargs.pop('show_grid', False)
auto_close = kwargs.get('auto_close')
pl = pyvista.Plotter(
window_size=window_size,
off_screen=off_screen,
notebook=notebook,
theme=theme,
border=border,
border_color=border_color,
border_width=border_width,
)
if show_axes is None:
show_axes = pl.theme.axes.show
if show_axes:
if pl.theme.axes.box:
pl.add_box_axes()
else:
pl.add_axes()
if anti_aliasing:
if anti_aliasing is True:
pl.enable_anti_aliasing('msaa', multi_samples=pyvista.global_theme.multi_samples)
else:
pl.enable_anti_aliasing(anti_aliasing)
elif anti_aliasing is False:
pl.disable_anti_aliasing()
try:
pl.set_background(background)
except (ValueError, TypeError):
if isinstance(background, (str, Path)):
path = Path(background)
if path.is_file():
pl.add_background_image(path)
else:
msg = f'Background must be color-like or a file path. Got {background} instead.'
raise TypeError(msg)
if isinstance(var_item, list):
if len(var_item) == 2: # might be arrows
isarr_0 = isinstance(var_item[0], np.ndarray)
isarr_1 = isinstance(var_item[1], np.ndarray)
if isarr_0 and isarr_1:
pl.add_arrows(var_item[0], var_item[1])
else:
for item in var_item:
if volume or (isinstance(item, np.ndarray) and item.ndim == 3):
pl.add_volume(item, **kwargs)
else:
pl.add_mesh(item, **kwargs)
else:
for item in var_item:
if volume or (isinstance(item, np.ndarray) and item.ndim == 3):
pl.add_volume(item, **kwargs)
else:
pl.add_mesh(item, **kwargs)
elif volume or (isinstance(var_item, np.ndarray) and var_item.ndim == 3):
pl.add_volume(var_item, **kwargs)
elif isinstance(var_item, pyvista.MultiBlock):
pl.add_composite(var_item, **kwargs)
else:
pl.add_mesh(var_item, **kwargs)
if text:
pl.add_text(text)
if show_grid:
pl.show_grid()
elif show_bounds:
pl.show_bounds()
if cpos is None:
cpos = pl.get_default_cam_pos()
pl.camera_position = cpos
pl.camera_set = False
else:
pl.camera_position = cpos
if eye_dome_lighting:
pl.enable_eye_dome_lighting()
if parallel_projection:
pl.enable_parallel_projection()
if ssao:
pl.enable_ssao()
if zoom is not None:
pl.camera.zoom(zoom)
return pl.show(
auto_close=auto_close,
interactive=interactive,
full_screen=full_screen,
screenshot=screenshot,
return_img=return_img,
jupyter_backend=jupyter_backend,
before_close_callback=before_close_callback,
jupyter_kwargs=jupyter_kwargs,
return_viewer=return_viewer,
return_cpos=return_cpos,
)
@@ -0,0 +1,22 @@
"""Version info for pyvista.
On the ``main`` branch, use 'dev0' to denote a development version.
For example:
version_info = 0, 27, 'dev0'
When generating pre-release wheels, use '0rcN', for example:
version_info = 0, 28, '0rc1'
Denotes the first release candidate.
"""
# major, minor, patch
from __future__ import annotations
version_info = 0, 46, 4
# Nice string for the version
__version__ = '.'.join(map(str, version_info))
@@ -0,0 +1,24 @@
"""Close all plotters to help control memory usage for our doctests."""
from __future__ import annotations
import pytest
import pyvista
@pytest.fixture(autouse=True)
def autoclose_plotters():
"""Close all plotters."""
yield
pyvista.close_all()
@pytest.fixture(autouse=True)
def reset_global_theme():
"""Reset global_theme."""
# this stops any doctest-module tests from overriding the global theme and
# creating test side effects
pyvista.set_plot_theme('document_build')
yield
pyvista.set_plot_theme('document_build')
@@ -0,0 +1,46 @@
"""Core routines."""
from __future__ import annotations
from . import _vtk_core as _vtk_core
from ._typing_core import *
from .cell import Cell as Cell
from .cell import CellArray as CellArray
from .celltype import CellType as CellType
from .composite import MultiBlock as MultiBlock
from .dataobject import DataObject as DataObject
from .dataset import DataSet as DataSet
from .datasetattributes import DataSetAttributes as DataSetAttributes
from .errors import AmbiguousDataError as AmbiguousDataError
from .errors import DeprecationError as DeprecationError
from .errors import MissingDataError as MissingDataError
from .errors import NotAllTrianglesError as NotAllTrianglesError
from .errors import PointSetCellOperationError as PointSetCellOperationError
from .errors import PointSetDimensionReductionError as PointSetDimensionReductionError
from .errors import PointSetNotSupported as PointSetNotSupported
from .errors import PyVistaAttributeError as PyVistaAttributeError
from .errors import PyVistaDeprecationWarning as PyVistaDeprecationWarning
from .errors import PyVistaEfficiencyWarning as PyVistaEfficiencyWarning
from .errors import PyVistaFutureWarning as PyVistaFutureWarning
from .errors import PyVistaPipelineError as PyVistaPipelineError
from .errors import VTKVersionError as VTKVersionError
from .filters import CompositeFilters as CompositeFilters
from .filters import DataObjectFilters as DataObjectFilters
from .filters import DataSetFilters as DataSetFilters
from .filters import ImageDataFilters as ImageDataFilters
from .filters import PolyDataFilters as PolyDataFilters
from .filters import UnstructuredGridFilters as UnstructuredGridFilters
from .grid import Grid as Grid
from .grid import ImageData as ImageData
from .grid import RectilinearGrid as RectilinearGrid
from .objects import Table as Table
from .partitioned import PartitionedDataSet as PartitionedDataSet
from .pointset import ExplicitStructuredGrid as ExplicitStructuredGrid
from .pointset import PointGrid as PointGrid
from .pointset import PointSet as PointSet
from .pointset import PolyData as PolyData
from .pointset import StructuredGrid as StructuredGrid
from .pointset import UnstructuredGrid as UnstructuredGrid
from .pyvista_ndarray import pyvista_ndarray as pyvista_ndarray
from .utilities import *
from .wrappers import _wrappers as _wrappers
@@ -0,0 +1,22 @@
"""Type aliases for type hints."""
from __future__ import annotations
from ._aliases import ArrayLike as ArrayLike
from ._aliases import BoundsTuple as BoundsTuple
from ._aliases import CellArrayLike as CellArrayLike
from ._aliases import CellsLike as CellsLike
from ._aliases import InteractionEventType as InteractionEventType
from ._aliases import MatrixLike as MatrixLike
from ._aliases import Number as Number
from ._aliases import RotationLike as RotationLike
from ._aliases import TransformLike as TransformLike
from ._aliases import VectorLike as VectorLike
from ._array_like import NumberType as NumberType
from ._array_like import NumpyArray as NumpyArray
from ._dataset_types import _DataObjectType as _DataObjectType
from ._dataset_types import _DataSetOrMultiBlockType as _DataSetOrMultiBlockType
from ._dataset_types import _DataSetType as _DataSetType
from ._dataset_types import _GridType as _GridType
from ._dataset_types import _PointGridType as _PointGridType
from ._dataset_types import _PointSetType as _PointSetType
@@ -0,0 +1,125 @@
"""Core type aliases."""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
from typing import Literal
from typing import NamedTuple
from typing import Union
from pyvista.core import _vtk_core as _vtk
from ._array_like import NumberType
from ._array_like import _ArrayLike
from ._array_like import _ArrayLike1D
from ._array_like import _ArrayLike2D
if TYPE_CHECKING or os.environ.get(
'PYVISTA_DOCUMENTATION_BULKY_IMPORTS_ALLOWED'
): # pragma: no cover
try:
from scipy.spatial.transform import Rotation
except ImportError:
Rotation = None
else:
Rotation = None
# NOTE:
# Type aliases are automatically expanded in the documentation.
# To document an alias as-is without expansion, the alias should be:
# (1) added to the "autodoc_type_aliases" dictionary in /doc/source/conf.py
# (2) added to /doc/core/typing.rst
# (3) added to the "numpydoc_validation" excludes in pyproject.toml
#
# Long or complex type aliases (e.g. a union of 4 or more base types) should
# always be added to the dictionary and documented
Number = Union[int, float]
VectorLike = _ArrayLike1D[NumberType]
VectorLike.__doc__ = """One-dimensional array-like object with numerical values.
Includes sequences and numpy arrays.
"""
MatrixLike = _ArrayLike2D[NumberType]
MatrixLike.__doc__ = """Two-dimensional array-like object with numerical values.
Includes singly-nested sequences and numpy arrays.
"""
ArrayLike = _ArrayLike[NumberType]
ArrayLike.__doc__ = """Any-dimensional array-like object with numerical values.
Includes sequences, nested sequences, and numpy arrays. Scalar values are not included.
"""
if Rotation is not None:
RotationLike = Union[MatrixLike[float], _vtk.vtkMatrix3x3, Rotation]
else:
RotationLike = Union[MatrixLike[float], _vtk.vtkMatrix3x3] # type: ignore[misc]
RotationLike.__doc__ = """Array or object representing a spatial rotation.
Includes 3x3 arrays and SciPy Rotation objects.
"""
TransformLike = Union[RotationLike, _vtk.vtkMatrix4x4, _vtk.vtkTransform]
TransformLike.__doc__ = """Array or object representing a spatial transformation.
Includes 3x3 and 4x4 arrays as well as SciPy Rotation objects."""
class BoundsTuple(NamedTuple):
"""Tuple of six values representing 3D bounds.
Has the form ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
"""
x_min: float
x_max: float
y_min: float
y_max: float
z_min: float
z_max: float
def __repr__(self) -> str:
# Split bounds at decimal and compute padding needed to the left of it
dot = '.'
strings = [str(float(val)) for val in self]
has_dot = [dot in s for s in strings]
split_strings = [s.split(dot) for s in strings]
pad_left = max(len(parts[0]) for parts in split_strings)
# Iterate through fields and align values at the decimal
lines = []
fields = self._fields
field_size = max(len(f) for f in fields)
name = self.__class__.__name__
whitespace = (len(name) + 1) * ' '
for i, items in enumerate(zip(fields, split_strings)):
field, parts = items
if has_dot[i]:
left, right = parts
aligned = f'{left:>{pad_left}}{dot}{right}'
else:
left = parts[0]
aligned = f'{left:>{pad_left}}'
spacing = '' if i == 0 else whitespace
comma = '' if i == len(fields) - 1 else ','
lines.append(f'{spacing}{field:<{field_size}} = {aligned}{comma}')
joined_lines = '\n'.join(lines)
return f'{name}({joined_lines})'
CellsLike = Union[MatrixLike[int], VectorLike[int]]
CellArrayLike = Union[CellsLike, _vtk.vtkCellArray]
# Undocumented alias - should be expanded in docs
_ArrayLikeOrScalar = Union[NumberType, ArrayLike[NumberType]]
InteractionEventType = Union[Literal['end', 'start', 'always'], _vtk.vtkCommand.EventIds]
InteractionEventType.__doc__ = """Interaction event mostly used for widgets.
Includes both strings such as `end`, 'start' and `always` and `_vtk.vtkCommand.EventIds`.
"""
@@ -0,0 +1,87 @@
"""Generic array-like type definitions.
Definitions here are loosely based on code in numpy._typing._array_like.
Some key differences include:
- Some npt._array_like definitions explicitly support dual-types for
handling python and numpy scalar data types separately.
Here, only a single generic type is used for simplicity.
- The npt._array_like definitions use a recursive _NestedSequence protocol.
Here, finite sequences are used instead.
- The npt._array_like definitions use a generic _SupportsArray protocol.
Here, we use `ndarray` directly.
- The npt._array_like definitions include scalar types (e.g. float, int).
Here they are excluded (i.e. scalars are not considered to be arrays).
- The npt._array_like TypeVar is bound to np.generic. Here, the
TypeVar is bound to a subset of numeric types only.
"""
from __future__ import annotations
from collections.abc import Sequence
from typing import TypeVar
from typing import Union
import numpy as np
import numpy.typing as npt
# Define numeric types
NumberType = TypeVar(
'NumberType',
bound=Union[np.floating, np.integer, np.bool_, float, int, bool],
)
NumberType.__doc__ = """Type variable for numeric data types."""
# Create a copy of the typevar which can be used for annotating a second variable.
# Its definition should be identical to `NumberType`
_NumberType = TypeVar( # noqa: PYI018
'_NumberType',
bound=Union[np.floating, np.integer, np.bool_, float, int, bool],
)
NumpyArray = npt.NDArray[NumberType]
_FiniteNestedList = Union[
list[NumberType],
list[list[NumberType]],
list[list[list[NumberType]]],
list[list[list[list[NumberType]]]],
]
_FiniteNestedTuple = Union[
tuple[NumberType],
tuple[tuple[NumberType]],
tuple[tuple[tuple[NumberType]]],
tuple[tuple[tuple[tuple[NumberType]]]],
]
_ArrayLike1D = Union[
NumpyArray[NumberType],
Sequence[NumberType],
Sequence[NumpyArray[NumberType]],
]
_ArrayLike2D = Union[
NumpyArray[NumberType],
Sequence[Sequence[NumberType]],
Sequence[Sequence[NumpyArray[NumberType]]],
]
_ArrayLike3D = Union[
NumpyArray[NumberType],
Sequence[Sequence[Sequence[NumberType]]],
Sequence[Sequence[Sequence[NumpyArray[NumberType]]]],
]
_ArrayLike4D = Union[
NumpyArray[NumberType],
Sequence[Sequence[Sequence[Sequence[NumberType]]]],
Sequence[Sequence[Sequence[Sequence[NumpyArray[NumberType]]]]],
]
_ArrayLike = Union[
_ArrayLike1D[NumberType],
_ArrayLike2D[NumberType],
_ArrayLike3D[NumberType],
_ArrayLike4D[NumberType],
]
@@ -0,0 +1,40 @@
"""PyVista dataset types."""
from __future__ import annotations
from typing import TypeVar
from typing import Union
from pyvista.core.composite import MultiBlock
from pyvista.core.dataobject import DataObject
from pyvista.core.dataset import DataSet
from pyvista.core.grid import Grid
from pyvista.core.pointset import PointGrid
from pyvista.core.pointset import PolyData
from pyvista.core.pointset import UnstructuredGrid
from pyvista.core.pointset import _PointSet
_GridType = TypeVar('_GridType', bound=Grid)
_GridType.__doc__ = """Type variable for PyVista ``Grid`` classes."""
_PointGridType = TypeVar('_PointGridType', bound=PointGrid)
_PointGridType.__doc__ = """Type variable for PyVista ``PointGrid`` classes."""
_PointSetType = TypeVar('_PointSetType', bound=_PointSet)
_PointSetType.__doc__ = """Type variable for PyVista ``PointSet`` classes."""
_DataSetType = TypeVar('_DataSetType', bound=DataSet)
_DataSetType.__doc__ = """Type variable for :class:`~pyvista.DataSet` classes."""
_DataSetOrMultiBlockType = TypeVar('_DataSetOrMultiBlockType', bound=Union[DataSet, MultiBlock])
_DataSetOrMultiBlockType.__doc__ = (
"""Type variable for :class:`~pyvista.DataSet` or :class:`~pyvista.MultiBlock` classes."""
)
_DataObjectType = TypeVar('_DataObjectType', bound=DataObject)
_DataObjectType.__doc__ = """Type variable for :class:`~pyvista.DataObject` classes."""
# Undocumented
_PolyDataType = TypeVar('_PolyDataType', bound=PolyData) # noqa: PYI018
_UnstructuredGridType = TypeVar('_UnstructuredGridType', bound=UnstructuredGrid) # noqa: PYI018
@@ -0,0 +1,36 @@
"""Input validation functions."""
from __future__ import annotations
from .check import check_contains as check_contains
from .check import check_finite as check_finite
from .check import check_greater_than as check_greater_than
from .check import check_instance as check_instance
from .check import check_integer as check_integer
from .check import check_iterable as check_iterable
from .check import check_iterable_items as check_iterable_items
from .check import check_length as check_length
from .check import check_less_than as check_less_than
from .check import check_ndim as check_ndim
from .check import check_nonnegative as check_nonnegative
from .check import check_number as check_number
from .check import check_range as check_range
from .check import check_real as check_real
from .check import check_sequence as check_sequence
from .check import check_shape as check_shape
from .check import check_sorted as check_sorted
from .check import check_string as check_string
from .check import check_subdtype as check_subdtype
from .check import check_type as check_type
from .validate import validate_array as validate_array
from .validate import validate_array3 as validate_array3
from .validate import validate_arrayN as validate_arrayN
from .validate import validate_arrayN_unsigned as validate_arrayN_unsigned
from .validate import validate_arrayNx3 as validate_arrayNx3
from .validate import validate_axes as validate_axes
from .validate import validate_data_range as validate_data_range
from .validate import validate_dimensionality as validate_dimensionality
from .validate import validate_number as validate_number
from .validate import validate_rotation as validate_rotation
from .validate import validate_transform3x3 as validate_transform3x3
from .validate import validate_transform4x4 as validate_transform4x4
@@ -0,0 +1,146 @@
"""Array casting functions."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Optional
from typing import Union
import numpy as np
import numpy.typing as npt
if TYPE_CHECKING:
from pyvista.core._typing_core import ArrayLike
from pyvista.core._typing_core import NumpyArray
from pyvista.core._typing_core._aliases import _ArrayLikeOrScalar
from pyvista.core._typing_core._array_like import NumberType
from pyvista.core._typing_core._array_like import _FiniteNestedList
from pyvista.core._typing_core._array_like import _FiniteNestedTuple
def _cast_to_list(
arr: _ArrayLikeOrScalar[NumberType],
) -> Union[NumberType, _FiniteNestedList[NumberType]]:
"""Cast an array to a nested list.
Parameters
----------
arr : float | ArrayLike[float]
Array to cast.
Returns
-------
list
List or nested list array.
"""
return _cast_to_numpy(arr).tolist()
def _cast_to_tuple(
arr: ArrayLike[NumberType],
) -> Union[NumberType, _FiniteNestedTuple[NumberType]]:
"""Cast an array to a nested tuple.
Parameters
----------
arr : float | ArrayLike[float]
Array to cast.
Returns
-------
tuple
Tuple or nested tuple array.
"""
arr = _cast_to_numpy(arr).tolist()
def _to_tuple(s):
return tuple(_to_tuple(i) for i in s) if isinstance(s, list) else s
return _to_tuple(arr)
def _cast_to_numpy(
arr: _ArrayLikeOrScalar[NumberType],
/,
*,
as_any: bool = True,
dtype: Optional[npt.DTypeLike] = None,
copy: bool = False,
must_be_real: bool = False,
) -> NumpyArray[NumberType]:
"""Cast array to a NumPy ndarray.
Object arrays are not allowed but the dtype is otherwise unchecked by default.
String arrays and complex numbers are therefore allowed.
.. warning::
Arrays intended for use with vtk should set ``must_be_real=True``
since ``numpy_to_vtk`` uses the array values directly without
checking for complex arrays.
Parameters
----------
arr : float | ArrayLike[float]
Array to cast.
as_any : bool, default: True
Allow subclasses of ``np.ndarray`` to pass through without
making a copy.
dtype : npt.typing.DTypeLike, optional
The data-type of the returned array.
copy : bool, default: False
If ``True``, a copy of the array is returned. A copy is always
returned if the array:
* is a nested sequence
* is a subclass of ``np.ndarray`` and ``as_any`` is ``False``.
must_be_real : bool, default: True
Raise a ``TypeError`` if the array does not have real numbers, i.e.
its data type is not integer or floating.
Raises
------
ValueError
If input cannot be cast as a NumPy ndarray.
TypeError
If an object array is created or if the data is not real numbers
and ``must_be_real`` is ``True``.
Returns
-------
np.ndarray
NumPy ndarray.
"""
# needed to support numpy <1.25
# needed to support vtk 9.0.3
# check for removal when support for vtk 9.0.3 is removed
try:
VisibleDeprecationWarning = np.exceptions.VisibleDeprecationWarning
except AttributeError:
# we only type for newer numpy, and this branch only touched in older numpy
if not TYPE_CHECKING:
VisibleDeprecationWarning = np.VisibleDeprecationWarning
try:
out = np.asanyarray(arr, dtype=dtype) if as_any else np.asarray(arr, dtype=dtype)
if copy and out is arr:
# we requested a copy but didn't end up with one
out = out.copy()
except (ValueError, VisibleDeprecationWarning) as e:
msg = f'Input cannot be cast as {np.ndarray}.'
raise ValueError(msg) from e
if must_be_real and not issubclass(out.dtype.type, (np.floating, np.integer)):
msg = f'Array must have real numbers. Got dtype {out.dtype.type}'
raise TypeError(msg)
elif out.dtype.name == 'object':
msg = f'Object arrays are not supported. Got {arr} when casting to a NumPy array.'
raise TypeError(msg)
return out
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,752 @@
"""Limited imports from VTK (excludes any GL-dependent).
These are the modules within VTK that must be loaded across pyvista's
core API. Here, we attempt to import modules using the ``vtkmodules``
package, which lets us only have to import from select modules and not
the entire library.
"""
from __future__ import annotations
import contextlib
import sys
from typing import NamedTuple
import warnings
from vtkmodules.vtkCommonCore import vtkInformation as vtkInformation
from vtkmodules.vtkCommonCore import vtkVersion as vtkVersion
from vtkmodules.vtkImagingSources import vtkImageEllipsoidSource as vtkImageEllipsoidSource
from vtkmodules.vtkImagingSources import vtkImageGaussianSource as vtkImageGaussianSource
from vtkmodules.vtkImagingSources import vtkImageGridSource as vtkImageGridSource
from vtkmodules.vtkImagingSources import vtkImageMandelbrotSource as vtkImageMandelbrotSource
from vtkmodules.vtkImagingSources import vtkImageNoiseSource as vtkImageNoiseSource
from vtkmodules.vtkImagingSources import vtkImageSinusoidSource as vtkImageSinusoidSource
# vtkExtractEdges moved from vtkFiltersExtraction to vtkFiltersCore in
# VTK commit d9981b9aeb93b42d1371c6e295d76bfdc18430bd
try:
from vtkmodules.vtkFiltersCore import vtkExtractEdges as vtkExtractEdges
except ImportError:
from vtkmodules.vtkFiltersExtraction import ( # type: ignore[attr-defined, no-redef]
vtkExtractEdges as vtkExtractEdges,
)
# vtkCellTreeLocator moved from vtkFiltersGeneral to vtkCommonDataModel in
# VTK commit 4a29e6f7dd9acb460644fe487d2e80aac65f7be9
try:
from vtkmodules.vtkCommonDataModel import vtkCellTreeLocator as vtkCellTreeLocator
except ImportError:
from vtkmodules.vtkFiltersGeneral import ( # type: ignore[attr-defined, no-redef]
vtkCellTreeLocator as vtkCellTreeLocator,
)
from vtkmodules.numpy_interface.dataset_adapter import VTKArray as VTKArray
from vtkmodules.numpy_interface.dataset_adapter import VTKObjectWrapper as VTKObjectWrapper
from vtkmodules.numpy_interface.dataset_adapter import numpyTovtkDataArray as numpyTovtkDataArray
from vtkmodules.util.numpy_support import get_vtk_array_type as get_vtk_array_type
from vtkmodules.util.numpy_support import numpy_to_vtk as numpy_to_vtk
from vtkmodules.util.numpy_support import numpy_to_vtkIdTypeArray as numpy_to_vtkIdTypeArray
from vtkmodules.util.numpy_support import vtk_to_numpy as vtk_to_numpy
with contextlib.suppress(ImportError):
from vtkmodules.util.pickle_support import (
serialize_VTK_data_object as serialize_VTK_data_object,
)
from vtkmodules.util.vtkAlgorithm import VTKPythonAlgorithmBase as VTKPythonAlgorithmBase
from vtkmodules.vtkCommonComputationalGeometry import vtkKochanekSpline as vtkKochanekSpline
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricBohemianDome as vtkParametricBohemianDome,
)
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricBour as vtkParametricBour
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricBoy as vtkParametricBoy
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricCatalanMinimal as vtkParametricCatalanMinimal,
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricConicSpiral as vtkParametricConicSpiral,
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricCrossCap as vtkParametricCrossCap,
)
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricDini as vtkParametricDini
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricEllipsoid as vtkParametricEllipsoid,
)
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricEnneper as vtkParametricEnneper
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricFigure8Klein as vtkParametricFigure8Klein,
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricFunction as vtkParametricFunction,
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricHenneberg as vtkParametricHenneberg,
)
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricKlein as vtkParametricKlein
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricKuen as vtkParametricKuen
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricMobius as vtkParametricMobius
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricPluckerConoid as vtkParametricPluckerConoid,
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricPseudosphere as vtkParametricPseudosphere,
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricRandomHills as vtkParametricRandomHills,
)
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricRoman as vtkParametricRoman
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricSpline as vtkParametricSpline
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricSuperEllipsoid as vtkParametricSuperEllipsoid,
)
from vtkmodules.vtkCommonComputationalGeometry import (
vtkParametricSuperToroid as vtkParametricSuperToroid,
)
from vtkmodules.vtkCommonComputationalGeometry import vtkParametricTorus as vtkParametricTorus
from vtkmodules.vtkCommonCore import VTK_ARIAL as VTK_ARIAL
from vtkmodules.vtkCommonCore import VTK_BIT as VTK_BIT
from vtkmodules.vtkCommonCore import VTK_CHAR as VTK_CHAR
from vtkmodules.vtkCommonCore import VTK_COURIER as VTK_COURIER
from vtkmodules.vtkCommonCore import VTK_DOUBLE as VTK_DOUBLE
from vtkmodules.vtkCommonCore import VTK_FLOAT as VTK_FLOAT
from vtkmodules.vtkCommonCore import VTK_FONT_FILE as VTK_FONT_FILE
from vtkmodules.vtkCommonCore import VTK_ID_TYPE as VTK_ID_TYPE
from vtkmodules.vtkCommonCore import VTK_INT as VTK_INT
from vtkmodules.vtkCommonCore import VTK_LONG as VTK_LONG
from vtkmodules.vtkCommonCore import VTK_LONG_LONG as VTK_LONG_LONG
from vtkmodules.vtkCommonCore import VTK_SHORT as VTK_SHORT
from vtkmodules.vtkCommonCore import VTK_SIGNED_CHAR as VTK_SIGNED_CHAR
from vtkmodules.vtkCommonCore import VTK_STRING as VTK_STRING
from vtkmodules.vtkCommonCore import VTK_TIMES as VTK_TIMES
from vtkmodules.vtkCommonCore import VTK_UNSIGNED_CHAR as VTK_UNSIGNED_CHAR
from vtkmodules.vtkCommonCore import VTK_UNSIGNED_INT as VTK_UNSIGNED_INT
from vtkmodules.vtkCommonCore import VTK_UNSIGNED_LONG as VTK_UNSIGNED_LONG
from vtkmodules.vtkCommonCore import VTK_UNSIGNED_LONG_LONG as VTK_UNSIGNED_LONG_LONG
from vtkmodules.vtkCommonCore import VTK_UNSIGNED_SHORT as VTK_UNSIGNED_SHORT
from vtkmodules.vtkCommonCore import buffer_shared as buffer_shared # type: ignore[attr-defined]
from vtkmodules.vtkCommonCore import mutable as mutable
from vtkmodules.vtkCommonCore import reference as reference
from vtkmodules.vtkCommonCore import vtkAbstractArray as vtkAbstractArray
from vtkmodules.vtkCommonCore import vtkBitArray as vtkBitArray
from vtkmodules.vtkCommonCore import vtkCharArray as vtkCharArray
from vtkmodules.vtkCommonCore import vtkCommand as vtkCommand
from vtkmodules.vtkCommonCore import vtkDataArray as vtkDataArray
from vtkmodules.vtkCommonCore import vtkDoubleArray as vtkDoubleArray
from vtkmodules.vtkCommonCore import vtkFileOutputWindow as vtkFileOutputWindow
from vtkmodules.vtkCommonCore import vtkFloatArray as vtkFloatArray
from vtkmodules.vtkCommonCore import vtkIdList as vtkIdList
from vtkmodules.vtkCommonCore import vtkIdTypeArray as vtkIdTypeArray
from vtkmodules.vtkCommonCore import vtkIntArray as vtkIntArray
from vtkmodules.vtkCommonCore import vtkLogger as vtkLogger
from vtkmodules.vtkCommonCore import vtkLongArray as vtkLongArray
from vtkmodules.vtkCommonCore import vtkLongLongArray as vtkLongLongArray
from vtkmodules.vtkCommonCore import vtkLookupTable as vtkLookupTable
from vtkmodules.vtkCommonCore import vtkMath as vtkMath
from vtkmodules.vtkCommonCore import vtkOutputWindow as vtkOutputWindow
from vtkmodules.vtkCommonCore import vtkPoints as vtkPoints
from vtkmodules.vtkCommonCore import vtkShortArray as vtkShortArray
from vtkmodules.vtkCommonCore import vtkSignedCharArray as vtkSignedCharArray
from vtkmodules.vtkCommonCore import vtkStringArray as vtkStringArray
from vtkmodules.vtkCommonCore import vtkStringOutputWindow as vtkStringOutputWindow
from vtkmodules.vtkCommonCore import vtkTypeInt32Array as vtkTypeInt32Array
from vtkmodules.vtkCommonCore import vtkTypeInt64Array as vtkTypeInt64Array
from vtkmodules.vtkCommonCore import vtkTypeUInt32Array as vtkTypeUInt32Array
from vtkmodules.vtkCommonCore import vtkUnsignedCharArray as vtkUnsignedCharArray
from vtkmodules.vtkCommonCore import vtkUnsignedIntArray as vtkUnsignedIntArray
from vtkmodules.vtkCommonCore import vtkUnsignedLongArray as vtkUnsignedLongArray
from vtkmodules.vtkCommonCore import vtkUnsignedLongLongArray as vtkUnsignedLongLongArray
from vtkmodules.vtkCommonCore import vtkUnsignedShortArray as vtkUnsignedShortArray
from vtkmodules.vtkCommonCore import vtkWeakReference as vtkWeakReference
from vtkmodules.vtkCommonDataModel import VTK_BEZIER_CURVE as VTK_BEZIER_CURVE
from vtkmodules.vtkCommonDataModel import VTK_BEZIER_HEXAHEDRON as VTK_BEZIER_HEXAHEDRON
from vtkmodules.vtkCommonDataModel import VTK_BEZIER_PYRAMID as VTK_BEZIER_PYRAMID
from vtkmodules.vtkCommonDataModel import VTK_BEZIER_QUADRILATERAL as VTK_BEZIER_QUADRILATERAL
from vtkmodules.vtkCommonDataModel import VTK_BEZIER_TETRAHEDRON as VTK_BEZIER_TETRAHEDRON
from vtkmodules.vtkCommonDataModel import VTK_BEZIER_TRIANGLE as VTK_BEZIER_TRIANGLE
from vtkmodules.vtkCommonDataModel import VTK_BEZIER_WEDGE as VTK_BEZIER_WEDGE
from vtkmodules.vtkCommonDataModel import VTK_BIQUADRATIC_QUAD as VTK_BIQUADRATIC_QUAD
from vtkmodules.vtkCommonDataModel import (
VTK_BIQUADRATIC_QUADRATIC_HEXAHEDRON as VTK_BIQUADRATIC_QUADRATIC_HEXAHEDRON,
)
from vtkmodules.vtkCommonDataModel import (
VTK_BIQUADRATIC_QUADRATIC_WEDGE as VTK_BIQUADRATIC_QUADRATIC_WEDGE,
)
from vtkmodules.vtkCommonDataModel import VTK_BIQUADRATIC_TRIANGLE as VTK_BIQUADRATIC_TRIANGLE
from vtkmodules.vtkCommonDataModel import VTK_CONVEX_POINT_SET as VTK_CONVEX_POINT_SET
from vtkmodules.vtkCommonDataModel import VTK_CUBIC_LINE as VTK_CUBIC_LINE
from vtkmodules.vtkCommonDataModel import VTK_EMPTY_CELL as VTK_EMPTY_CELL
from vtkmodules.vtkCommonDataModel import VTK_HEXAGONAL_PRISM as VTK_HEXAGONAL_PRISM
from vtkmodules.vtkCommonDataModel import VTK_HEXAHEDRON as VTK_HEXAHEDRON
from vtkmodules.vtkCommonDataModel import VTK_HIGHER_ORDER_EDGE as VTK_HIGHER_ORDER_EDGE
from vtkmodules.vtkCommonDataModel import (
VTK_HIGHER_ORDER_HEXAHEDRON as VTK_HIGHER_ORDER_HEXAHEDRON,
)
from vtkmodules.vtkCommonDataModel import VTK_HIGHER_ORDER_POLYGON as VTK_HIGHER_ORDER_POLYGON
from vtkmodules.vtkCommonDataModel import VTK_HIGHER_ORDER_PYRAMID as VTK_HIGHER_ORDER_PYRAMID
from vtkmodules.vtkCommonDataModel import VTK_HIGHER_ORDER_QUAD as VTK_HIGHER_ORDER_QUAD
from vtkmodules.vtkCommonDataModel import (
VTK_HIGHER_ORDER_TETRAHEDRON as VTK_HIGHER_ORDER_TETRAHEDRON,
)
from vtkmodules.vtkCommonDataModel import VTK_HIGHER_ORDER_TRIANGLE as VTK_HIGHER_ORDER_TRIANGLE
from vtkmodules.vtkCommonDataModel import VTK_HIGHER_ORDER_WEDGE as VTK_HIGHER_ORDER_WEDGE
from vtkmodules.vtkCommonDataModel import VTK_LAGRANGE_CURVE as VTK_LAGRANGE_CURVE
from vtkmodules.vtkCommonDataModel import VTK_LAGRANGE_HEXAHEDRON as VTK_LAGRANGE_HEXAHEDRON
from vtkmodules.vtkCommonDataModel import VTK_LAGRANGE_PYRAMID as VTK_LAGRANGE_PYRAMID
from vtkmodules.vtkCommonDataModel import VTK_LAGRANGE_QUADRILATERAL as VTK_LAGRANGE_QUADRILATERAL
from vtkmodules.vtkCommonDataModel import VTK_LAGRANGE_TETRAHEDRON as VTK_LAGRANGE_TETRAHEDRON
from vtkmodules.vtkCommonDataModel import VTK_LAGRANGE_TRIANGLE as VTK_LAGRANGE_TRIANGLE
from vtkmodules.vtkCommonDataModel import VTK_LAGRANGE_WEDGE as VTK_LAGRANGE_WEDGE
from vtkmodules.vtkCommonDataModel import VTK_LINE as VTK_LINE
from vtkmodules.vtkCommonDataModel import VTK_PARAMETRIC_CURVE as VTK_PARAMETRIC_CURVE
from vtkmodules.vtkCommonDataModel import VTK_PARAMETRIC_HEX_REGION as VTK_PARAMETRIC_HEX_REGION
from vtkmodules.vtkCommonDataModel import (
VTK_PARAMETRIC_QUAD_SURFACE as VTK_PARAMETRIC_QUAD_SURFACE,
)
from vtkmodules.vtkCommonDataModel import VTK_PARAMETRIC_SURFACE as VTK_PARAMETRIC_SURFACE
from vtkmodules.vtkCommonDataModel import (
VTK_PARAMETRIC_TETRA_REGION as VTK_PARAMETRIC_TETRA_REGION,
)
from vtkmodules.vtkCommonDataModel import VTK_PARAMETRIC_TRI_SURFACE as VTK_PARAMETRIC_TRI_SURFACE
from vtkmodules.vtkCommonDataModel import VTK_PENTAGONAL_PRISM as VTK_PENTAGONAL_PRISM
from vtkmodules.vtkCommonDataModel import VTK_PIXEL as VTK_PIXEL
from vtkmodules.vtkCommonDataModel import VTK_POLY_LINE as VTK_POLY_LINE
from vtkmodules.vtkCommonDataModel import VTK_POLY_VERTEX as VTK_POLY_VERTEX
from vtkmodules.vtkCommonDataModel import VTK_POLYGON as VTK_POLYGON
from vtkmodules.vtkCommonDataModel import VTK_POLYHEDRON as VTK_POLYHEDRON
from vtkmodules.vtkCommonDataModel import VTK_PYRAMID as VTK_PYRAMID
from vtkmodules.vtkCommonDataModel import VTK_QUAD as VTK_QUAD
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_EDGE as VTK_QUADRATIC_EDGE
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_HEXAHEDRON as VTK_QUADRATIC_HEXAHEDRON
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_LINEAR_QUAD as VTK_QUADRATIC_LINEAR_QUAD
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_LINEAR_WEDGE as VTK_QUADRATIC_LINEAR_WEDGE
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_POLYGON as VTK_QUADRATIC_POLYGON
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_PYRAMID as VTK_QUADRATIC_PYRAMID
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_QUAD as VTK_QUADRATIC_QUAD
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_TETRA as VTK_QUADRATIC_TETRA
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_TRIANGLE as VTK_QUADRATIC_TRIANGLE
from vtkmodules.vtkCommonDataModel import VTK_QUADRATIC_WEDGE as VTK_QUADRATIC_WEDGE
from vtkmodules.vtkCommonDataModel import VTK_TETRA as VTK_TETRA
from vtkmodules.vtkCommonDataModel import VTK_TRIANGLE as VTK_TRIANGLE
from vtkmodules.vtkCommonDataModel import VTK_TRIANGLE_STRIP as VTK_TRIANGLE_STRIP
from vtkmodules.vtkCommonDataModel import (
VTK_TRIQUADRATIC_HEXAHEDRON as VTK_TRIQUADRATIC_HEXAHEDRON,
)
from vtkmodules.vtkCommonDataModel import VTK_VERTEX as VTK_VERTEX
from vtkmodules.vtkCommonDataModel import VTK_VOXEL as VTK_VOXEL
from vtkmodules.vtkCommonDataModel import VTK_WEDGE as VTK_WEDGE
from vtkmodules.vtkCommonDataModel import vtkAbstractCellLocator as vtkAbstractCellLocator
from vtkmodules.vtkCommonDataModel import vtkBezierCurve as vtkBezierCurve
from vtkmodules.vtkCommonDataModel import vtkBezierHexahedron as vtkBezierHexahedron
from vtkmodules.vtkCommonDataModel import vtkBezierQuadrilateral as vtkBezierQuadrilateral
from vtkmodules.vtkCommonDataModel import vtkBezierTetra as vtkBezierTetra
from vtkmodules.vtkCommonDataModel import vtkBezierTriangle as vtkBezierTriangle
from vtkmodules.vtkCommonDataModel import vtkBezierWedge as vtkBezierWedge
from vtkmodules.vtkCommonDataModel import vtkBiQuadraticQuad as vtkBiQuadraticQuad
from vtkmodules.vtkCommonDataModel import (
vtkBiQuadraticQuadraticHexahedron as vtkBiQuadraticQuadraticHexahedron,
)
from vtkmodules.vtkCommonDataModel import (
vtkBiQuadraticQuadraticWedge as vtkBiQuadraticQuadraticWedge,
)
from vtkmodules.vtkCommonDataModel import vtkBiQuadraticTriangle as vtkBiQuadraticTriangle
from vtkmodules.vtkCommonDataModel import vtkCell as vtkCell
from vtkmodules.vtkCommonDataModel import vtkCellArray as vtkCellArray
from vtkmodules.vtkCommonDataModel import vtkCellLocator as vtkCellLocator
from vtkmodules.vtkCommonDataModel import vtkColor3ub as vtkColor3ub
from vtkmodules.vtkCommonDataModel import vtkCompositeDataSet as vtkCompositeDataSet
from vtkmodules.vtkCommonDataModel import vtkConvexPointSet as vtkConvexPointSet
from vtkmodules.vtkCommonDataModel import vtkCubicLine as vtkCubicLine
from vtkmodules.vtkCommonDataModel import vtkDataObject as vtkDataObject
from vtkmodules.vtkCommonDataModel import vtkDataSet as vtkDataSet
from vtkmodules.vtkCommonDataModel import vtkDataSetAttributes as vtkDataSetAttributes
from vtkmodules.vtkCommonDataModel import vtkEmptyCell as vtkEmptyCell
from vtkmodules.vtkCommonDataModel import vtkExplicitStructuredGrid as vtkExplicitStructuredGrid
from vtkmodules.vtkCommonDataModel import vtkFieldData as vtkFieldData
from vtkmodules.vtkCommonDataModel import vtkGenericCell as vtkGenericCell
from vtkmodules.vtkCommonDataModel import vtkHexagonalPrism as vtkHexagonalPrism
from vtkmodules.vtkCommonDataModel import vtkHexahedron as vtkHexahedron
from vtkmodules.vtkCommonDataModel import vtkImageData as vtkImageData
from vtkmodules.vtkCommonDataModel import vtkImplicitFunction as vtkImplicitFunction
from vtkmodules.vtkCommonDataModel import (
vtkIterativeClosestPointTransform as vtkIterativeClosestPointTransform,
)
from vtkmodules.vtkCommonDataModel import vtkLagrangeCurve as vtkLagrangeCurve
from vtkmodules.vtkCommonDataModel import vtkLagrangeHexahedron as vtkLagrangeHexahedron
from vtkmodules.vtkCommonDataModel import vtkLagrangeQuadrilateral as vtkLagrangeQuadrilateral
from vtkmodules.vtkCommonDataModel import vtkLagrangeTriangle as vtkLagrangeTriangle
from vtkmodules.vtkCommonDataModel import vtkLagrangeWedge as vtkLagrangeWedge
from vtkmodules.vtkCommonDataModel import vtkLine as vtkLine
from vtkmodules.vtkCommonDataModel import vtkMultiBlockDataSet as vtkMultiBlockDataSet
from vtkmodules.vtkCommonDataModel import vtkNonMergingPointLocator as vtkNonMergingPointLocator
from vtkmodules.vtkCommonDataModel import vtkPartitionedDataSet as vtkPartitionedDataSet
from vtkmodules.vtkCommonDataModel import vtkPentagonalPrism as vtkPentagonalPrism
from vtkmodules.vtkCommonDataModel import vtkPerlinNoise as vtkPerlinNoise
from vtkmodules.vtkCommonDataModel import vtkPiecewiseFunction as vtkPiecewiseFunction
from vtkmodules.vtkCommonDataModel import vtkPixel as vtkPixel
from vtkmodules.vtkCommonDataModel import vtkPlane as vtkPlane
from vtkmodules.vtkCommonDataModel import vtkPlaneCollection as vtkPlaneCollection
from vtkmodules.vtkCommonDataModel import vtkPlanes as vtkPlanes
from vtkmodules.vtkCommonDataModel import vtkPointLocator as vtkPointLocator
from vtkmodules.vtkCommonDataModel import vtkPointSet as vtkPointSet
from vtkmodules.vtkCommonDataModel import vtkPolyData as vtkPolyData
from vtkmodules.vtkCommonDataModel import vtkPolygon as vtkPolygon
from vtkmodules.vtkCommonDataModel import vtkPolyhedron as vtkPolyhedron
from vtkmodules.vtkCommonDataModel import vtkPolyLine as vtkPolyLine
from vtkmodules.vtkCommonDataModel import vtkPolyPlane as vtkPolyPlane
from vtkmodules.vtkCommonDataModel import vtkPolyVertex as vtkPolyVertex
from vtkmodules.vtkCommonDataModel import vtkPyramid as vtkPyramid
from vtkmodules.vtkCommonDataModel import vtkQuad as vtkQuad
from vtkmodules.vtkCommonDataModel import vtkQuadraticEdge as vtkQuadraticEdge
from vtkmodules.vtkCommonDataModel import vtkQuadraticHexahedron as vtkQuadraticHexahedron
from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearQuad as vtkQuadraticLinearQuad
from vtkmodules.vtkCommonDataModel import vtkQuadraticLinearWedge as vtkQuadraticLinearWedge
from vtkmodules.vtkCommonDataModel import vtkQuadraticPolygon as vtkQuadraticPolygon
from vtkmodules.vtkCommonDataModel import vtkQuadraticPyramid as vtkQuadraticPyramid
from vtkmodules.vtkCommonDataModel import vtkQuadraticQuad as vtkQuadraticQuad
from vtkmodules.vtkCommonDataModel import vtkQuadraticTetra as vtkQuadraticTetra
from vtkmodules.vtkCommonDataModel import vtkQuadraticTriangle as vtkQuadraticTriangle
from vtkmodules.vtkCommonDataModel import vtkQuadraticWedge as vtkQuadraticWedge
from vtkmodules.vtkCommonDataModel import vtkRectf as vtkRectf
from vtkmodules.vtkCommonDataModel import vtkRectilinearGrid as vtkRectilinearGrid
from vtkmodules.vtkCommonDataModel import vtkSelection as vtkSelection
from vtkmodules.vtkCommonDataModel import vtkSelectionNode as vtkSelectionNode
from vtkmodules.vtkCommonDataModel import vtkStaticCellLocator as vtkStaticCellLocator
from vtkmodules.vtkCommonDataModel import vtkStaticPointLocator as vtkStaticPointLocator
from vtkmodules.vtkCommonDataModel import vtkStructuredGrid as vtkStructuredGrid
from vtkmodules.vtkCommonDataModel import vtkStructuredPoints as vtkStructuredPoints
from vtkmodules.vtkCommonDataModel import vtkTable as vtkTable
from vtkmodules.vtkCommonDataModel import vtkTetra as vtkTetra
from vtkmodules.vtkCommonDataModel import vtkTriangle as vtkTriangle
from vtkmodules.vtkCommonDataModel import vtkTriangleStrip as vtkTriangleStrip
from vtkmodules.vtkCommonDataModel import vtkTriQuadraticHexahedron as vtkTriQuadraticHexahedron
from vtkmodules.vtkCommonDataModel import vtkUnstructuredGrid as vtkUnstructuredGrid
from vtkmodules.vtkCommonDataModel import vtkVertex as vtkVertex
from vtkmodules.vtkCommonDataModel import vtkVoxel as vtkVoxel
from vtkmodules.vtkCommonDataModel import vtkWedge as vtkWedge
with contextlib.suppress(ImportError): # Introduced prior to VTK 9.2
from vtkmodules.vtkCommonDataModel import VTK_TRIQUADRATIC_PYRAMID as VTK_TRIQUADRATIC_PYRAMID
from vtkmodules.vtkCommonDataModel import vtkTriQuadraticPyramid as vtkTriQuadraticPyramid
from vtkmodules.vtkCommonExecutionModel import vtkAlgorithm as vtkAlgorithm
from vtkmodules.vtkCommonExecutionModel import vtkAlgorithmOutput as vtkAlgorithmOutput
from vtkmodules.vtkCommonExecutionModel import vtkCompositeDataPipeline as vtkCompositeDataPipeline
from vtkmodules.vtkCommonExecutionModel import vtkImageToStructuredGrid as vtkImageToStructuredGrid
from vtkmodules.vtkCommonMath import vtkMatrix3x3 as vtkMatrix3x3
from vtkmodules.vtkCommonMath import vtkMatrix4x4 as vtkMatrix4x4
from vtkmodules.vtkCommonTransforms import vtkTransform as vtkTransform
from vtkmodules.vtkDomainsChemistry import vtkProteinRibbonFilter as vtkProteinRibbonFilter
from vtkmodules.vtkFiltersCore import VTK_BEST_FITTING_PLANE as VTK_BEST_FITTING_PLANE
from vtkmodules.vtkFiltersCore import vtkAppendArcLength as vtkAppendArcLength
from vtkmodules.vtkFiltersCore import vtkAppendFilter as vtkAppendFilter
from vtkmodules.vtkFiltersCore import vtkAppendPolyData as vtkAppendPolyData
from vtkmodules.vtkFiltersCore import vtkCellCenters as vtkCellCenters
from vtkmodules.vtkFiltersCore import vtkCellDataToPointData as vtkCellDataToPointData
from vtkmodules.vtkFiltersCore import vtkCenterOfMass as vtkCenterOfMass
from vtkmodules.vtkFiltersCore import vtkCleanPolyData as vtkCleanPolyData
from vtkmodules.vtkFiltersCore import vtkClipPolyData as vtkClipPolyData
from vtkmodules.vtkFiltersCore import vtkConnectivityFilter as vtkConnectivityFilter
from vtkmodules.vtkFiltersCore import vtkContourFilter as vtkContourFilter
from vtkmodules.vtkFiltersCore import vtkCutter as vtkCutter
from vtkmodules.vtkFiltersCore import vtkDecimatePolylineFilter as vtkDecimatePolylineFilter
from vtkmodules.vtkFiltersCore import vtkDecimatePro as vtkDecimatePro
from vtkmodules.vtkFiltersCore import vtkDelaunay2D as vtkDelaunay2D
from vtkmodules.vtkFiltersCore import vtkDelaunay3D as vtkDelaunay3D
from vtkmodules.vtkFiltersCore import vtkElevationFilter as vtkElevationFilter
from vtkmodules.vtkFiltersCore import (
vtkExplicitStructuredGridToUnstructuredGrid as vtkExplicitStructuredGridToUnstructuredGrid,
)
from vtkmodules.vtkFiltersCore import vtkFeatureEdges as vtkFeatureEdges
from vtkmodules.vtkFiltersCore import vtkFlyingEdges3D as vtkFlyingEdges3D
from vtkmodules.vtkFiltersCore import vtkGlyph3D as vtkGlyph3D
from vtkmodules.vtkFiltersCore import vtkImplicitPolyDataDistance as vtkImplicitPolyDataDistance
from vtkmodules.vtkFiltersCore import vtkMarchingCubes as vtkMarchingCubes
from vtkmodules.vtkFiltersCore import vtkMassProperties as vtkMassProperties
with contextlib.suppress(ImportError): # Introduced VTK 9.4
from vtkmodules.vtkFiltersCore import vtkOrientPolyData as vtkOrientPolyData
from vtkmodules.vtkFiltersCore import vtkPointDataToCellData as vtkPointDataToCellData
from vtkmodules.vtkFiltersCore import vtkPolyDataNormals as vtkPolyDataNormals
from vtkmodules.vtkFiltersCore import vtkQuadricDecimation as vtkQuadricDecimation
from vtkmodules.vtkFiltersCore import vtkResampleWithDataSet as vtkResampleWithDataSet
from vtkmodules.vtkFiltersCore import vtkReverseSense as vtkReverseSense
from vtkmodules.vtkFiltersCore import vtkSmoothPolyDataFilter as vtkSmoothPolyDataFilter
from vtkmodules.vtkFiltersCore import vtkStripper as vtkStripper
from vtkmodules.vtkFiltersCore import vtkThreshold as vtkThreshold
from vtkmodules.vtkFiltersCore import vtkTriangleFilter as vtkTriangleFilter
from vtkmodules.vtkFiltersCore import vtkTubeFilter as vtkTubeFilter
from vtkmodules.vtkFiltersCore import (
vtkUnstructuredGridToExplicitStructuredGrid as vtkUnstructuredGridToExplicitStructuredGrid,
)
from vtkmodules.vtkFiltersCore import (
vtkWindowedSincPolyDataFilter as vtkWindowedSincPolyDataFilter,
)
from vtkmodules.vtkFiltersExtraction import vtkExtractCellsByType as vtkExtractCellsByType
from vtkmodules.vtkFiltersExtraction import vtkExtractGeometry as vtkExtractGeometry
from vtkmodules.vtkFiltersExtraction import vtkExtractGrid as vtkExtractGrid
from vtkmodules.vtkFiltersExtraction import vtkExtractSelection as vtkExtractSelection
from vtkmodules.vtkFiltersFlowPaths import (
vtkEvenlySpacedStreamlines2D as vtkEvenlySpacedStreamlines2D,
)
from vtkmodules.vtkFiltersFlowPaths import vtkStreamTracer as vtkStreamTracer
with contextlib.suppress(ImportError): # Introduced VTK v9.1.0
from vtkmodules.vtkFiltersGeneral import vtkRemovePolyData as vtkRemovePolyData
from vtkmodules.vtkFiltersGeneral import vtkAxes as vtkAxes
from vtkmodules.vtkFiltersGeneral import (
vtkBooleanOperationPolyDataFilter as vtkBooleanOperationPolyDataFilter,
)
from vtkmodules.vtkFiltersGeneral import vtkBoxClipDataSet as vtkBoxClipDataSet
from vtkmodules.vtkFiltersGeneral import vtkClipClosedSurface as vtkClipClosedSurface
from vtkmodules.vtkFiltersGeneral import vtkContourTriangulator as vtkContourTriangulator
from vtkmodules.vtkFiltersGeneral import vtkCursor3D as vtkCursor3D
from vtkmodules.vtkFiltersGeneral import vtkCurvatures as vtkCurvatures
from vtkmodules.vtkFiltersGeneral import vtkDataSetTriangleFilter as vtkDataSetTriangleFilter
from vtkmodules.vtkFiltersGeneral import vtkGradientFilter as vtkGradientFilter
from vtkmodules.vtkFiltersGeneral import (
vtkIntersectionPolyDataFilter as vtkIntersectionPolyDataFilter,
)
from vtkmodules.vtkFiltersGeneral import vtkOBBTree as vtkOBBTree
from vtkmodules.vtkFiltersGeneral import (
vtkRectilinearGridToPointSet as vtkRectilinearGridToPointSet,
)
from vtkmodules.vtkFiltersGeneral import (
vtkRectilinearGridToTetrahedra as vtkRectilinearGridToTetrahedra,
)
from vtkmodules.vtkFiltersGeneral import vtkShrinkFilter as vtkShrinkFilter
from vtkmodules.vtkFiltersGeneral import vtkTableBasedClipDataSet as vtkTableBasedClipDataSet
from vtkmodules.vtkFiltersGeneral import vtkTableToPolyData as vtkTableToPolyData
from vtkmodules.vtkFiltersGeneral import vtkTessellatorFilter as vtkTessellatorFilter
from vtkmodules.vtkFiltersGeneral import vtkTransformFilter as vtkTransformFilter
from vtkmodules.vtkFiltersGeneral import vtkWarpScalar as vtkWarpScalar
from vtkmodules.vtkFiltersGeneral import vtkWarpVector as vtkWarpVector
from vtkmodules.vtkFiltersGeometry import (
vtkCompositeDataGeometryFilter as vtkCompositeDataGeometryFilter,
)
from vtkmodules.vtkFiltersGeometry import vtkDataSetSurfaceFilter as vtkDataSetSurfaceFilter
from vtkmodules.vtkFiltersGeometry import vtkGeometryFilter as vtkGeometryFilter
from vtkmodules.vtkFiltersGeometry import (
vtkStructuredGridGeometryFilter as vtkStructuredGridGeometryFilter,
)
from vtkmodules.vtkFiltersHybrid import vtkPolyDataSilhouette as vtkPolyDataSilhouette
from vtkmodules.vtkFiltersModeling import (
vtkAdaptiveSubdivisionFilter as vtkAdaptiveSubdivisionFilter,
)
from vtkmodules.vtkFiltersModeling import (
vtkBandedPolyDataContourFilter as vtkBandedPolyDataContourFilter,
)
from vtkmodules.vtkFiltersModeling import (
vtkButterflySubdivisionFilter as vtkButterflySubdivisionFilter,
)
from vtkmodules.vtkFiltersModeling import (
vtkCollisionDetectionFilter as vtkCollisionDetectionFilter,
)
from vtkmodules.vtkFiltersModeling import (
vtkDijkstraGraphGeodesicPath as vtkDijkstraGraphGeodesicPath,
)
from vtkmodules.vtkFiltersModeling import vtkFillHolesFilter as vtkFillHolesFilter
from vtkmodules.vtkFiltersModeling import vtkLinearExtrusionFilter as vtkLinearExtrusionFilter
from vtkmodules.vtkFiltersModeling import vtkLinearSubdivisionFilter as vtkLinearSubdivisionFilter
from vtkmodules.vtkFiltersModeling import vtkLoopSubdivisionFilter as vtkLoopSubdivisionFilter
from vtkmodules.vtkFiltersModeling import vtkOutlineFilter as vtkOutlineFilter
from vtkmodules.vtkFiltersModeling import vtkRibbonFilter as vtkRibbonFilter
from vtkmodules.vtkFiltersModeling import (
vtkRotationalExtrusionFilter as vtkRotationalExtrusionFilter,
)
from vtkmodules.vtkFiltersModeling import vtkRuledSurfaceFilter as vtkRuledSurfaceFilter
from vtkmodules.vtkFiltersModeling import vtkSelectEnclosedPoints as vtkSelectEnclosedPoints
from vtkmodules.vtkFiltersModeling import vtkSubdivideTetra as vtkSubdivideTetra
from vtkmodules.vtkFiltersModeling import vtkTrimmedExtrusionFilter as vtkTrimmedExtrusionFilter
from vtkmodules.vtkFiltersParallel import vtkIntegrateAttributes as vtkIntegrateAttributes
with contextlib.suppress(ImportError):
# `vtkmodules.vtkFiltersParallelDIY2` is unavailable in some versions of `vtk` from conda-forge
from vtkmodules.vtkFiltersParallelDIY2 import (
vtkRedistributeDataSetFilter as vtkRedistributeDataSetFilter,
)
from vtkmodules.vtkFiltersPoints import vtkGaussianKernel as vtkGaussianKernel
from vtkmodules.vtkFiltersPoints import vtkPointInterpolator as vtkPointInterpolator
from vtkmodules.vtkFiltersSources import vtkArcSource as vtkArcSource
from vtkmodules.vtkFiltersSources import vtkArrowSource as vtkArrowSource
with contextlib.suppress(ImportError):
# Deprecated in 9.3
from vtkmodules.vtkFiltersSources import ( # type: ignore[attr-defined]
vtkCapsuleSource as vtkCapsuleSource,
)
from vtkmodules.vtkFiltersSources import vtkConeSource as vtkConeSource
from vtkmodules.vtkFiltersSources import vtkCubeSource as vtkCubeSource
from vtkmodules.vtkFiltersSources import vtkCylinderSource as vtkCylinderSource
from vtkmodules.vtkFiltersSources import vtkDiskSource as vtkDiskSource
from vtkmodules.vtkFiltersSources import vtkFrustumSource as vtkFrustumSource
from vtkmodules.vtkFiltersSources import vtkLineSource as vtkLineSource
from vtkmodules.vtkFiltersSources import vtkOutlineCornerFilter as vtkOutlineCornerFilter
from vtkmodules.vtkFiltersSources import vtkOutlineCornerSource as vtkOutlineCornerSource
from vtkmodules.vtkFiltersSources import vtkParametricFunctionSource as vtkParametricFunctionSource
from vtkmodules.vtkFiltersSources import vtkPlaneSource as vtkPlaneSource
from vtkmodules.vtkFiltersSources import vtkPlatonicSolidSource as vtkPlatonicSolidSource
from vtkmodules.vtkFiltersSources import vtkPointSource as vtkPointSource
from vtkmodules.vtkFiltersSources import vtkRegularPolygonSource as vtkRegularPolygonSource
from vtkmodules.vtkFiltersSources import vtkSphereSource as vtkSphereSource
from vtkmodules.vtkFiltersSources import vtkSuperquadricSource as vtkSuperquadricSource
from vtkmodules.vtkFiltersSources import vtkTessellatedBoxSource as vtkTessellatedBoxSource
from vtkmodules.vtkFiltersStatistics import vtkComputeQuartiles as vtkComputeQuartiles
with contextlib.suppress(ImportError):
from vtkmodules.vtkFiltersStatistics import vtkLengthDistribution as vtkLengthDistribution
from vtkmodules.vtkFiltersTexture import vtkTextureMapToPlane as vtkTextureMapToPlane
from vtkmodules.vtkFiltersTexture import vtkTextureMapToSphere as vtkTextureMapToSphere
from vtkmodules.vtkFiltersVerdict import vtkCellQuality as vtkCellQuality
from vtkmodules.vtkFiltersVerdict import vtkCellSizeFilter as vtkCellSizeFilter
from vtkmodules.vtkFiltersVerdict import vtkMeshQuality as vtkMeshQuality
with contextlib.suppress(ImportError):
from vtkmodules.vtkFiltersVerdict import vtkBoundaryMeshQuality as vtkBoundaryMeshQuality
from vtkmodules.vtkImagingCore import vtkAbstractImageInterpolator as vtkAbstractImageInterpolator
from vtkmodules.vtkImagingCore import vtkExtractVOI as vtkExtractVOI
from vtkmodules.vtkImagingCore import vtkImageConstantPad as vtkImageConstantPad
from vtkmodules.vtkImagingCore import vtkImageDifference as vtkImageDifference
from vtkmodules.vtkImagingCore import vtkImageExtractComponents as vtkImageExtractComponents
from vtkmodules.vtkImagingCore import vtkImageFlip as vtkImageFlip
from vtkmodules.vtkImagingCore import vtkImageInterpolator as vtkImageInterpolator
from vtkmodules.vtkImagingCore import vtkImageMirrorPad as vtkImageMirrorPad
from vtkmodules.vtkImagingCore import vtkImageResize as vtkImageResize
from vtkmodules.vtkImagingCore import vtkImageSincInterpolator as vtkImageSincInterpolator
from vtkmodules.vtkImagingCore import vtkImageThreshold as vtkImageThreshold
from vtkmodules.vtkImagingCore import vtkImageWrapPad as vtkImageWrapPad
from vtkmodules.vtkImagingCore import vtkRTAnalyticSource as vtkRTAnalyticSource
from vtkmodules.vtkImagingGeneral import vtkImageGaussianSmooth as vtkImageGaussianSmooth
from vtkmodules.vtkImagingGeneral import vtkImageMedian3D as vtkImageMedian3D
from vtkmodules.vtkImagingHybrid import vtkGaussianSplatter as vtkGaussianSplatter
from vtkmodules.vtkImagingHybrid import vtkSampleFunction as vtkSampleFunction
from vtkmodules.vtkImagingHybrid import (
vtkSurfaceReconstructionFilter as vtkSurfaceReconstructionFilter,
)
from vtkmodules.vtkImagingMorphological import (
vtkImageConnectivityFilter as vtkImageConnectivityFilter,
)
from vtkmodules.vtkImagingStencil import vtkImageStencil as vtkImageStencil
from vtkmodules.vtkImagingStencil import vtkPolyDataToImageStencil as vtkPolyDataToImageStencil
from vtkmodules.vtkIOGeometry import vtkHoudiniPolyDataWriter as vtkHoudiniPolyDataWriter
from vtkmodules.vtkIOGeometry import vtkIVWriter as vtkIVWriter
from vtkmodules.vtkIOGeometry import vtkOBJWriter as vtkOBJWriter
from vtkmodules.vtkIOGeometry import vtkProStarReader as vtkProStarReader
from vtkmodules.vtkIOGeometry import vtkSTLWriter as vtkSTLWriter
with contextlib.suppress(ImportError): # Introduced VTK v9.4.0
from vtkmodules.vtkIOHDF import vtkHDFWriter as vtkHDFWriter
from vtkmodules.vtkIOInfovis import vtkDelimitedTextReader as vtkDelimitedTextReader
from vtkmodules.vtkIOLegacy import vtkDataReader as vtkDataReader
from vtkmodules.vtkIOLegacy import vtkDataSetReader as vtkDataSetReader
from vtkmodules.vtkIOLegacy import vtkDataSetWriter as vtkDataSetWriter
from vtkmodules.vtkIOLegacy import vtkDataWriter as vtkDataWriter
from vtkmodules.vtkIOLegacy import vtkPolyDataReader as vtkPolyDataReader
from vtkmodules.vtkIOLegacy import vtkPolyDataWriter as vtkPolyDataWriter
from vtkmodules.vtkIOLegacy import vtkRectilinearGridReader as vtkRectilinearGridReader
from vtkmodules.vtkIOLegacy import vtkRectilinearGridWriter as vtkRectilinearGridWriter
from vtkmodules.vtkIOLegacy import vtkSimplePointsWriter as vtkSimplePointsWriter
from vtkmodules.vtkIOLegacy import vtkStructuredGridReader as vtkStructuredGridReader
from vtkmodules.vtkIOLegacy import vtkStructuredGridWriter as vtkStructuredGridWriter
from vtkmodules.vtkIOLegacy import vtkUnstructuredGridReader as vtkUnstructuredGridReader
from vtkmodules.vtkIOLegacy import vtkUnstructuredGridWriter as vtkUnstructuredGridWriter
from vtkmodules.vtkIOPLY import vtkPLYReader as vtkPLYReader
from vtkmodules.vtkIOPLY import vtkPLYWriter as vtkPLYWriter
from vtkmodules.vtkIOXML import vtkXMLImageDataReader as vtkXMLImageDataReader
from vtkmodules.vtkIOXML import vtkXMLImageDataWriter as vtkXMLImageDataWriter
from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataReader as vtkXMLMultiBlockDataReader
from vtkmodules.vtkIOXML import vtkXMLMultiBlockDataWriter as vtkXMLMultiBlockDataWriter
from vtkmodules.vtkIOXML import vtkXMLPartitionedDataSetReader as vtkXMLPartitionedDataSetReader
from vtkmodules.vtkIOXML import vtkXMLPImageDataReader as vtkXMLPImageDataReader
from vtkmodules.vtkIOXML import vtkXMLPolyDataReader as vtkXMLPolyDataReader
from vtkmodules.vtkIOXML import vtkXMLPolyDataWriter as vtkXMLPolyDataWriter
from vtkmodules.vtkIOXML import vtkXMLPRectilinearGridReader as vtkXMLPRectilinearGridReader
from vtkmodules.vtkIOXML import vtkXMLPUnstructuredGridReader as vtkXMLPUnstructuredGridReader
from vtkmodules.vtkIOXML import vtkXMLReader as vtkXMLReader
from vtkmodules.vtkIOXML import vtkXMLRectilinearGridReader as vtkXMLRectilinearGridReader
from vtkmodules.vtkIOXML import vtkXMLRectilinearGridWriter as vtkXMLRectilinearGridWriter
from vtkmodules.vtkIOXML import vtkXMLStructuredGridReader as vtkXMLStructuredGridReader
from vtkmodules.vtkIOXML import vtkXMLStructuredGridWriter as vtkXMLStructuredGridWriter
from vtkmodules.vtkIOXML import vtkXMLTableReader as vtkXMLTableReader
from vtkmodules.vtkIOXML import vtkXMLTableWriter as vtkXMLTableWriter
from vtkmodules.vtkIOXML import vtkXMLUnstructuredGridReader as vtkXMLUnstructuredGridReader
from vtkmodules.vtkIOXML import vtkXMLUnstructuredGridWriter as vtkXMLUnstructuredGridWriter
from vtkmodules.vtkIOXML import vtkXMLWriter as vtkXMLWriter
with contextlib.suppress(ImportError):
from vtkmodules.vtkImagingMorphological import vtkImageDilateErode3D as vtkImageDilateErode3D
try:
from vtkmodules.vtkPythonContext2D import vtkPythonItem as vtkPythonItem
except ImportError: # pragma: no cover
# `vtkmodules.vtkPythonContext2D` is unavailable in some versions of `vtk` (see #3224)
class vtkPythonItem: # type: ignore[no-redef] # noqa: N801
"""Empty placeholder."""
def __init__(self): # pragma: no cover
"""Raise version error on init."""
from pyvista.core.errors import VTKVersionError # noqa: PLC0415
msg = 'Chart backgrounds require the vtkPythonContext2D module'
raise VTKVersionError(msg)
from vtkmodules.vtkImagingFourier import vtkImageButterworthHighPass as vtkImageButterworthHighPass
from vtkmodules.vtkImagingFourier import vtkImageButterworthLowPass as vtkImageButterworthLowPass
from vtkmodules.vtkImagingFourier import vtkImageFFT as vtkImageFFT
from vtkmodules.vtkImagingFourier import vtkImageRFFT as vtkImageRFFT
# 9.1+ imports
with contextlib.suppress(ImportError):
from vtkmodules.vtkFiltersPoints import vtkConvertToPointCloud as vtkConvertToPointCloud
with contextlib.suppress(ImportError): # Introduced prior to VTK 9.3
from vtkmodules.vtkRenderingCore import vtkViewport as vtkViewport
# 9.3+ imports
with contextlib.suppress(ImportError):
from vtkmodules.vtkFiltersCore import vtkPackLabels as vtkPackLabels
from vtkmodules.vtkFiltersCore import vtkSurfaceNets3D as vtkSurfaceNets3D
# 9.1+ imports
with contextlib.suppress(ImportError):
from vtkmodules.vtkIOParallelXML import (
vtkXMLPartitionedDataSetWriter as vtkXMLPartitionedDataSetWriter,
)
class VersionInfo(NamedTuple):
"""Version information as a named tuple."""
major: int
minor: int
micro: int
def __str__(self):
return str((self.major, self.minor, self.micro))
def VTKVersionInfo(): # noqa: N802
"""Return the vtk version as a namedtuple.
Returns
-------
VersionInfo
Version information as a named tuple.
"""
try:
ver = vtkVersion()
major = ver.GetVTKMajorVersion()
minor = ver.GetVTKMinorVersion()
micro = ver.GetVTKBuildVersion()
except AttributeError: # pragma: no cover
warnings.warn('Unable to detect VTK version. Defaulting to v4.0.0')
major, minor, micro = (4, 0, 0)
return VersionInfo(major, minor, micro)
vtk_version_info = VTKVersionInfo()
class vtkPyVistaOverride: # noqa: N801
"""Base class to automatically override VTK classes with PyVista classes."""
def __init_subclass__(cls, **kwargs):
if vtk_version_info >= (9, 4):
# Check for VTK base classes and call the override method
for base in cls.__bases__:
if (
hasattr(base, '__module__')
and base.__module__.startswith('vtkmodules.')
and hasattr(base, 'override')
):
# For now, just remove any overrides for these classes
# There are clear issues with the current implementation
# of overriding these classes upstream and until they are
# resolved, we will entirely remove the overrides.
# See https://gitlab.kitware.com/vtk/vtk/-/merge_requests/11698
# See https://gitlab.kitware.com/vtk/vtk/-/issues/19550#note_1598883
base.override(None)
break
return cls
class DisableVtkSnakeCase:
"""Base class to raise error if using VTK's `snake_case` API."""
@staticmethod
def check_attribute(target, attr):
# Check sys.meta_path to avoid dynamic imports when Python is shutting down
if vtk_version_info >= (9, 4) and sys.meta_path is not None:
# Raise error if accessing attributes from VTK's pythonic snake_case API
import pyvista as pv # noqa: PLC0415
state = pv._VTK_SNAKE_CASE_STATE
if state != 'allow':
if (
attr not in ['__class__', '__init__']
and attr[0].islower()
and is_vtk_attribute(target, attr)
):
msg = (
f'The attribute {attr!r} is defined by VTK and is not part of the '
f'PyVista API'
)
if state == 'error':
raise pv.PyVistaAttributeError(msg)
else:
warnings.warn(msg, RuntimeWarning)
def __getattribute__(self, item):
DisableVtkSnakeCase.check_attribute(self, item)
return object.__getattribute__(self, item)
def is_vtk_attribute(obj: object, attr: str): # numpydoc ignore=RT01
"""Return True if the attribute is defined by a vtk class.
Parameters
----------
obj : object
Class or instance to check.
attr : str
Name of the attribute to check.
"""
def _find_defining_class(cls, attr):
"""Find the class that defines a given attribute."""
for base in cls.__mro__:
if attr in base.__dict__:
return base
return None
cls = _find_defining_class(obj if isinstance(obj, type) else obj.__class__, attr)
return cls is not None and cls.__module__.startswith('vtkmodules')
class VTKObjectWrapperCheckSnakeCase(VTKObjectWrapper):
"""Superclass for classes that wrap VTK objects with Python objects.
This class overrides __getattr__ to disable the VTK snake case API.
"""
def __getattr__(self, name: str):
"""Forward unknown attribute requests to VTKArray's __getattr__."""
if self.VTKObject is not None:
# Check if forwarding snake_case attributes
DisableVtkSnakeCase.check_attribute(self.VTKObject, name)
return getattr(self.VTKObject, name)
raise AttributeError
@@ -0,0 +1,904 @@
"""Contains the pyvista.Cell class."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import cast
import warnings
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from . import _vtk_core as _vtk
from ._typing_core import BoundsTuple
from .celltype import CellType
from .dataobject import DataObject
from .errors import CellSizeError
from .errors import PyVistaDeprecationWarning
from .utilities.cells import numpy_to_idarr
from .utilities.misc import _BoundsSizeMixin
from .utilities.misc import _NoNewAttrMixin
if TYPE_CHECKING:
from typing import Any
from typing_extensions import Self
from pyvista import UnstructuredGrid
from ._typing_core import CellsLike
from ._typing_core import MatrixLike
from ._typing_core import NumpyArray
def _get_vtk_id_type() -> type[np.int32 | np.int64]:
"""Return the numpy datatype responding to :vtk:`vtkIdTypeArray`."""
VTK_ID_TYPE_SIZE = _vtk.vtkIdTypeArray().GetDataTypeSize()
if VTK_ID_TYPE_SIZE == 4:
return np.int32
elif VTK_ID_TYPE_SIZE == 8:
return np.int64
return np.int32
class Cell(_BoundsSizeMixin, DataObject, _vtk.vtkGenericCell):
"""Wrapping of :vtk:`vtkCell`.
This class provides the capability to access a given cell topology and can
be useful when walking through a cell's individual faces or investigating
cell properties.
Parameters
----------
vtk_cell : :vtk:`vtkCell`, optional
The vtk object to wrap as Cell, that must be of :vtk:`vtkCell` type.
cell_type : int, optional
VTK cell type. Determined from ``vtk_cell`` if not input.
deep : bool, default: False
Perform a deep copy of the original cell.
Notes
-----
Accessing individual cells from a :class:`pyvista.DataSet` using this class
will be much slower than accessing bulk data from the
:attr:`pyvista.PolyData.faces` or :attr:`pyvista.UnstructuredGrid.cells` attributes.
Also note that the cell object is a deep copy of the original cell and
is unassociated with the original cell. Changing any data of
that cell (for example, :attr:`pyvista.Cell.points`) will not change the original dataset.
Examples
--------
Get the 0-th cell from a :class:`pyvista.PolyData`.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> cell = mesh.get_cell(0)
>>> cell # doctest: +SKIP
Cell (0x7fa760075a10)
Type: <CellType.TRIANGLE: 5>
Linear: True
Dimension: 2
N Points: 3
N Faces: 0
N Edges: 3
X Bounds: -5.406e-02, -5.551e-17
Y Bounds: 0.000e+00, 1.124e-02
Z Bounds: -5.000e-01, -4.971e-01
Get the 0-th cell from a :class:`pyvista.UnstructuredGrid`.
>>> from pyvista import examples
>>> mesh = examples.load_hexbeam()
>>> cell = mesh.get_cell(0)
>>> cell # doctest: +SKIP
Cell (0x7fdc71a3c210)
Type: <CellType.HEXAHEDRON: 12>
Linear: True
Dimension: 3
N Points: 8
N Faces: 6
N Edges: 12
X Bounds: 0.000e+00, 5.000e-01
Y Bounds: 0.000e+00, 5.000e-01
Z Bounds: 0.000e+00, 5.000e-01
"""
@_deprecate_positional_args(allowed=['vtk_cell', 'cell_type'])
def __init__(
self: Self,
vtk_cell: _vtk.vtkCell | None = None,
cell_type: CellType | None = None,
deep: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize the cell."""
super().__init__()
if vtk_cell is not None:
if not isinstance(vtk_cell, _vtk.vtkCell):
msg = f'`vtk_cell` must be a vtkCell, not {type(vtk_cell)}' # type: ignore[unreachable]
raise TypeError(msg)
# cell type must be set first before deep or shallow copy
if cell_type is None:
self.SetCellType(vtk_cell.GetCellType())
else:
self.SetCellType(cell_type)
if deep:
self.DeepCopy(vtk_cell)
else:
self.ShallowCopy(vtk_cell)
@property
def type(self: Self) -> CellType:
"""Get the cell type from the enum :class:`pyvista.CellType`.
Returns
-------
pyvista.CellType
Type of cell.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).type
<CellType.TRIANGLE: 5>
"""
return CellType(self.GetCellType())
@property
def is_linear(self: Self) -> bool:
"""Return if the cell is linear.
Returns
-------
bool
If the cell is linear.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).is_linear
True
"""
return bool(self.IsLinear())
def plot(self: Self, **kwargs) -> None:
"""Plot this cell.
Parameters
----------
**kwargs : dict, optional
See :func:`pyvista.plot` for a description of the optional keyword
arguments.
Examples
--------
>>> from pyvista import examples
>>> mesh = examples.load_hexbeam()
>>> cell = mesh.get_cell(0)
>>> cell.plot()
"""
self.cast_to_unstructured_grid().plot(**kwargs)
def cast_to_polydata(self: Self) -> pyvista.PolyData:
"""Cast this cell to PolyData.
Can only be used for 0D, 1D, or 2D cells.
Returns
-------
pyvista.PolyData
This cell cast to a :class:`pyvista.PolyData`.
Examples
--------
>>> from pyvista import examples
>>> mesh = examples.load_sphere()
>>> cell = mesh.get_cell(0)
>>> grid = cell.cast_to_polydata()
>>> grid # doctest: +SKIP
PolyData (0x7f09ae437b80)
N Cells: 1
N Points: 3
N Strips: 0
X Bounds: 0.000e+00, 1.000e+01
Y Bounds: 0.000e+00, 2.500e+01
Z Bounds: -1.270e+02, -1.250e+02
N Arrays: 0
"""
cells = [len(self.point_ids), *list(range(len(self.point_ids)))]
if self.dimension == 0:
return pyvista.PolyData(self.points.copy(), verts=cells)
if self.dimension == 1:
return pyvista.PolyData(self.points.copy(), lines=cells)
if self.dimension == 2:
if self.type == CellType.TRIANGLE_STRIP:
return pyvista.PolyData(self.points.copy(), strips=cells)
else:
return pyvista.PolyData(self.points.copy(), faces=cells)
else:
msg = f'3D cells cannot be cast to PolyData: got cell type {self.type}'
raise ValueError(msg)
def cast_to_unstructured_grid(self: Self) -> UnstructuredGrid:
"""Cast this cell to an unstructured grid.
Returns
-------
pyvista.UnstructuredGrid
This cell cast to a :class:`pyvista.UnstructuredGrid`.
Examples
--------
>>> from pyvista import examples
>>> mesh = examples.load_hexbeam()
>>> cell = mesh.get_cell(0)
>>> grid = cell.cast_to_unstructured_grid()
>>> grid # doctest: +SKIP
UnstructuredGrid (0x7f9383619540)
N Cells: 1
N Points: 8
X Bounds: 0.000e+00, 5.000e-01
Y Bounds: 0.000e+00, 5.000e-01
Z Bounds: 0.000e+00, 5.000e-01
N Arrays: 0
"""
if self.type == CellType.POLYHEDRON:
# construct from faces
cell_ids = [self.n_faces]
for face in self.faces:
cell_ids.append(len(face.point_ids))
cell_ids.extend(self.point_ids.index(i) for i in face.point_ids)
cell_ids.insert(0, len(cell_ids))
else:
cell_ids = [len(self.point_ids), *list(range(len(self.point_ids)))]
return pyvista.UnstructuredGrid(
cell_ids,
[int(self.type)],
self.points.copy(),
)
@property
def dimension(self: Self) -> int:
"""Return the cell dimension.
This returns the dimensionality of the cell. For example, 1 for an edge,
2 for a triangle, and 3 for a tetrahedron.
Returns
-------
int
The cell dimension.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).dimension
2
"""
return self.GetCellDimension()
@property
def n_points(self: Self) -> int:
"""Get the number of points composing the cell.
Returns
-------
int
The number of points.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).n_points
3
"""
return self.GetNumberOfPoints()
@property
def n_faces(self: Self) -> int:
"""Get the number of faces composing the cell.
Returns
-------
int
The number of faces.
Examples
--------
>>> from pyvista.examples.cells import Tetrahedron
>>> mesh = Tetrahedron()
>>> mesh.get_cell(0).n_faces
4
"""
return self.GetNumberOfFaces()
@property
def n_edges(self: Self) -> int:
"""Get the number of edges composing the cell.
Returns
-------
int
The number of edges composing the cell.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).n_edges
3
"""
return self.GetNumberOfEdges()
@property
def point_ids(self: Self) -> list[int]:
"""Get the point IDs composing the cell.
Returns
-------
list[int]
The point IDs composing the cell.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).point_ids
[2, 30, 0]
"""
point_ids = self.GetPointIds()
return [point_ids.GetId(i) for i in range(point_ids.GetNumberOfIds())]
@property
def points(self: Self) -> NumpyArray[float]:
"""Get the point coordinates of the cell.
Returns
-------
np.ndarray
The point coordinates of the cell.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).points
array([[0.05405951, 0. , 0.49706897],
[0.05287818, 0.0112396 , 0.49706897],
[0. , 0. , 0.5 ]])
"""
return _vtk.vtk_to_numpy(self.GetPoints().GetData())
def get_edge(self: Self, index: int) -> Cell:
"""Get the i-th edge composing the cell.
Parameters
----------
index : int
Edge ID.
Returns
-------
pyvista.Cell
Edge given by ``index``.
Examples
--------
Extract a single edge from a face and output the IDs of the edge
points.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> cell = mesh.get_cell(0)
>>> edge = cell.get_edge(0)
>>> edge.point_ids
[2, 30]
"""
if index + 1 > self.n_edges:
msg = f'Invalid index {index} for a cell with {self.n_edges} edges.'
raise IndexError(msg)
# must deep copy here as multiple sequental calls to GetEdge overwrite
# the underlying pointer
return Cell(self.GetEdge(index), deep=True) # type: ignore[abstract]
@property
def edges(self: Self) -> list[Cell]:
"""Return a list of edges composing the cell.
Returns
-------
list[Cell]
A list of edges composing the cell.
Examples
--------
>>> from pyvista.examples.cells import Hexahedron
>>> mesh = Hexahedron()
>>> cell = mesh.get_cell(0)
>>> edges = cell.edges
>>> len(edges)
12
"""
return [self.get_edge(i) for i in range(self.n_edges)]
@property
def faces(self: Self) -> list[Cell]:
"""Return a list of faces composing the cell.
Returns
-------
list[Cell]
A list of faces composing the cell.
Examples
--------
>>> from pyvista.examples.cells import Tetrahedron
>>> mesh = Tetrahedron()
>>> cell = mesh.get_cell(0)
>>> faces = cell.faces
>>> len(faces)
4
"""
return [self.get_face(i) for i in range(self.n_faces)]
def get_face(self: Self, index: int) -> Cell:
"""Get the i-th face composing the cell.
Parameters
----------
index : int
Face ID.
Returns
-------
pyvista.Cell
Face given by ``index``.
Examples
--------
Return the face IDs composing the first face of an example tetrahedron.
>>> from pyvista.examples.cells import Tetrahedron
>>> mesh = Tetrahedron()
>>> cell = mesh.get_cell(0)
>>> face = cell.get_face(0)
>>> face.point_ids
[0, 1, 3]
"""
# must deep copy here as sequental calls overwrite the underlying pointer
if index + 1 > self.n_faces:
msg = f'Invalid index {index} for a cell with {self.n_faces} faces.'
raise IndexError(msg)
# must deep copy here as multiple sequental calls to GetFace overwrite
# the underlying pointer
cell = self.GetFace(index)
return Cell(cell, deep=True, cell_type=cast('CellType', cell.GetCellType())) # type: ignore[abstract]
@property
def bounds(self: Self) -> BoundsTuple:
"""Get the cell bounds in ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
Returns
-------
BoundsTuple
The cell bounds in ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).bounds
BoundsTuple(x_min = 0.0,
x_max = 0.05405950918793678,
y_min = 0.0,
y_max = 0.011239604093134403,
z_min = 0.49706897139549255,
z_max = 0.5)
"""
return BoundsTuple(*self.GetBounds())
@property
def center(self: Self) -> tuple[float, float, float]:
"""Get the center of the cell.
Uses parametric coordinate center to determine x-y-z center.
Returns
-------
tuple[float, float, float]
The center of the cell.
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.get_cell(0).center
(0.03564589594801267, 0.0037465346977114677, 0.49804598093032837)
"""
para_center = [0.0, 0.0, 0.0]
sub_id = self.GetParametricCenter(para_center)
# EvaluateLocation requires mutable sub_id
sub_id = _vtk.mutable(sub_id) # type: ignore[assignment]
# center and weights are returned from EvaluateLocation
center = [0.0, 0.0, 0.0]
weights = [0.0] * self.n_points
self.EvaluateLocation(sub_id, para_center, center, weights)
return cast('tuple[float, float, float]', tuple(center))
def _get_attrs(self: Self) -> list[tuple[str, Any, str]]:
"""Return the representation methods (internal helper)."""
attrs = []
attrs.append(('Type', repr(self.type), '{}' * len(repr(self.type))))
attrs.append(('Linear', self.is_linear, '{}')) # type: ignore[arg-type]
attrs.append(('Dimension', self.dimension, '{}')) # type: ignore[arg-type]
attrs.append(('N Points', self.n_points, '{}')) # type: ignore[arg-type]
attrs.append(('N Faces', self.n_faces, '{}')) # type: ignore[arg-type]
attrs.append(('N Edges', self.n_edges, '{}')) # type: ignore[arg-type]
bds = self.bounds
fmt = f'{pyvista.FLOAT_FORMAT}, {pyvista.FLOAT_FORMAT}'
attrs.append(('X Bounds', (bds[0], bds[1]), fmt)) # type: ignore[arg-type]
attrs.append(('Y Bounds', (bds[2], bds[3]), fmt)) # type: ignore[arg-type]
attrs.append(('Z Bounds', (bds[4], bds[5]), fmt)) # type: ignore[arg-type]
return attrs
def __repr__(self: Self) -> str:
"""Return the object representation."""
return self.head(display=False, html=False)
def __str__(self: Self) -> str:
"""Return the object string representation."""
return self.head(display=False, html=False)
@_deprecate_positional_args
def copy(self: Self, deep: bool = True) -> Self: # noqa: FBT001, FBT002
"""Return a copy of the cell.
Parameters
----------
deep : bool, optional
When ``True`` makes a full copy of the cell. When ``False``,
performs a shallow copy where the new cell still references the
original cell.
Returns
-------
pyvista.Cell
Deep or shallow copy of the cell.
Examples
--------
Create a deep copy of the cell and demonstrate it is deep.
>>> from pyvista.examples.cells import Tetrahedron
>>> mesh = Tetrahedron()
>>> cell = mesh.get_cell(0)
>>> deep_cell = cell.copy(deep=True)
>>> deep_cell.points[:] = 0
>>> cell != deep_cell
True
Create a shallow copy of the cell and demonstrate it is shallow.
>>> shallow_cell = cell.copy(deep=False)
>>> shallow_cell.points[:] = 0
>>> cell == shallow_cell
True
"""
return type(self)(self, deep=deep)
class CellArray(
_NoNewAttrMixin,
_vtk.DisableVtkSnakeCase,
_vtk.vtkPyVistaOverride,
_vtk.vtkCellArray,
):
"""PyVista wrapping of :vtk:`vtkCellArray`.
Provides convenience functions to simplify creating a CellArray from
a numpy array or list.
.. deprecated:: 0.44.0
The parameters ``n_cells`` and ``deep`` are deprecated and no longer used.
Parameters
----------
cells : np.ndarray or list, optional
Import an array of data with the legacy :vtk:`vtkCellArray` layout, e.g.
``{ n0, p0_0, p0_1, ..., p0_n, n1, p1_0, p1_1, ..., p1_n, ... }``
Where n0 is the number of points in cell 0, and pX_Y is the Y'th
point in cell X.
n_cells : int, optional
The number of cells.
deep : bool, default: False
Perform a deep copy of the original cell.
Examples
--------
Create a cell array containing two triangles from the traditional interleaved format
>>> from pyvista.core.cell import CellArray
>>> cellarr = CellArray([3, 0, 1, 2, 3, 3, 4, 5])
Create a cell array containing two triangles from separate offsets and connectivity arrays
>>> from pyvista.core.cell import CellArray
>>> offsets = [0, 3, 6]
>>> connectivity = [0, 1, 2, 3, 4, 5]
>>> cellarr = CellArray.from_arrays(offsets, connectivity)
"""
@_deprecate_positional_args(allowed=['cells'])
def __init__(
self: Self,
cells: CellsLike | None = None,
n_cells: int | None = None,
deep: bool | None = None, # noqa: FBT001
) -> None:
"""Initialize a :vtk:`vtkCellArray`."""
super().__init__()
self.__offsets: _vtk.vtkIdTypeArray | None = None
self.__connectivity: _vtk.vtkIdTypeArray | None = None
if cells is not None:
self.cells = cells
# deprecated 0.44.0, convert to error in 0.47.0, remove 0.48.0
for k, v in (('n_cells', n_cells), ('deep', deep)):
if v is not None:
warnings.warn(
f'`CellArray parameter `{k}` is deprecated and no longer used.',
PyVistaDeprecationWarning,
)
@property
def cells(self: Self) -> NumpyArray[int]:
"""Return a numpy array of the cells.
Returns
-------
np.ndarray
A numpy array of the cells.
"""
cells = _vtk.vtkIdTypeArray()
self.ExportLegacyFormat(cells)
return _vtk.vtk_to_numpy(cells)
@cells.setter
def cells(self: Self, cells: CellsLike) -> None:
cells = np.asarray(cells)
vtk_idarr = numpy_to_idarr(cells, deep=False, return_ind=False)
self.ImportLegacyFormat(vtk_idarr)
imported_size = self.GetNumberOfConnectivityEntries()
# https://github.com/pyvista/pyvista/pull/5404
if imported_size != cells.size:
msg = (
f'Cell array size is invalid. Size ({cells.size}) does not'
f' match expected size ({imported_size}). This is likely'
' due to invalid connectivity array.'
)
raise CellSizeError(msg)
self.__offsets = self.__connectivity = None
@property
def n_cells(self: Self) -> int:
"""Return the number of cells.
Returns
-------
int
The number of cells.
"""
return self.GetNumberOfCells()
@property
def connectivity_array(self: Self) -> NumpyArray[int]:
"""Return the array with the point ids that define the cells' connectivity.
Returns
-------
np.ndarray
Array with the point ids that define the cells' connectivity.
"""
return _get_connectivity_array(self)
@property
def offset_array(self: Self) -> NumpyArray[int]:
"""Return the array used to store cell offsets.
Returns
-------
np.ndarray
Array used to store cell offsets.
"""
return _get_offset_array(self)
def _set_data(
self: Self,
offsets: MatrixLike[int],
connectivity: MatrixLike[int],
*,
deep: bool = False,
) -> None:
"""Set the offsets and connectivity arrays."""
vtk_offsets = numpy_to_idarr(offsets, deep=deep)
vtk_connectivity = numpy_to_idarr(connectivity, deep=deep)
self.SetData(vtk_offsets, vtk_connectivity)
# Because vtkCellArray doesn't take ownership of the arrays, it's possible for them to get
# garbage collected. Keep a reference to them for safety
self.__offsets = vtk_offsets
self.__connectivity = vtk_connectivity
@staticmethod
@_deprecate_positional_args(allowed=['offsets', 'connectivity'])
def from_arrays(
offsets: MatrixLike[int],
connectivity: MatrixLike[int],
deep: bool = False, # noqa: FBT001, FBT002
) -> CellArray:
"""Construct a CellArray from offsets and connectivity arrays.
Parameters
----------
offsets : MatrixLike[int]
Offsets array of length `n_cells + 1`.
connectivity : MatrixLike[int]
Connectivity array.
deep : bool, default: False
Whether to deep copy the array data into the vtk arrays.
Returns
-------
CellArray
Constructed CellArray.
"""
cellarr = CellArray()
cellarr._set_data(offsets, connectivity, deep=deep)
return cellarr
@property
def regular_cells(self: Self) -> NumpyArray[int]:
"""Return a (n_cells, cell_size)-shaped array of point indices for equal-sized faces.
Returns
-------
numpy.ndarray
Array of face indices of shape (n_cells, cell_size).
Notes
-----
This property does not validate that the cells are all
actually the same size. If they're not, this property may either
raise a `ValueError` or silently return an incorrect array.
"""
return _get_regular_cells(self)
@classmethod
@_deprecate_positional_args(allowed=['cells'])
def from_regular_cells(
cls: type[CellArray],
cells: MatrixLike[int],
deep: bool = False, # noqa: FBT001, FBT002
) -> pyvista.CellArray:
"""Construct a ``CellArray`` from a (n_cells, cell_size) array of cell indices.
Parameters
----------
cells : numpy.ndarray or list[list[int]]
Cell array of shape (n_cells, cell_size) where all cells have the same `cell_size`.
deep : bool, default: False
Whether to deep copy the cell array data into the vtk connectivity array.
Returns
-------
pyvista.CellArray
Constructed ``CellArray``.
"""
cells = np.asarray(cells, dtype=pyvista.ID_TYPE)
n_cells, cell_size = cells.shape
offsets = cell_size * np.arange(n_cells + 1, dtype=pyvista.ID_TYPE)
cellarr = cls()
cellarr._set_data(offsets, cells, deep=deep)
return cellarr
@classmethod
def from_irregular_cells(cls: type[CellArray], cells: MatrixLike[int]) -> pyvista.CellArray:
"""Construct a ``CellArray`` from a (n_cells, cell_size) array of cell indices.
Parameters
----------
cells : numpy.ndarray or list[list[int]]
Cell array of shape (n_cells, cell_size) where all cells have the same `cell_size`.
Returns
-------
pyvista.CellArray
Constructed ``CellArray``.
"""
offsets = np.cumsum([len(c) for c in cells])
offsets = np.concatenate([[0], offsets], dtype=pyvista.ID_TYPE)
connectivity = np.concatenate(cells, dtype=pyvista.ID_TYPE)
return cls.from_arrays(offsets, connectivity) # type: ignore[arg-type]
# The following methods would be much nicer bound to CellArray,
# but then they wouldn't be available on bare vtkCellArrays. In the future,
# consider using vtkCellArray.override decorator, so they're all automatically
# returned as CellArrays
def _get_connectivity_array(cellarr: _vtk.vtkCellArray) -> NumpyArray[int]:
"""Return the array with the point ids that define the cells' connectivity."""
return _vtk.vtk_to_numpy(cellarr.GetConnectivityArray())
def _get_offset_array(cellarr: _vtk.vtkCellArray) -> NumpyArray[int]:
"""Return the array used to store cell offsets."""
return _vtk.vtk_to_numpy(cellarr.GetOffsetsArray())
def _get_regular_cells(cellarr: _vtk.vtkCellArray) -> NumpyArray[int]:
"""Return a (n_cells, cell_size)-shaped array of point indices for equal-sized faces."""
cells = _get_connectivity_array(cellarr)
if len(cells) == 0:
return cells
offsets = _get_offset_array(cellarr)
cell_size = offsets[1] - offsets[0]
return cells.reshape(-1, cell_size)
def _get_irregular_cells(cellarr: _vtk.vtkCellArray) -> tuple[NumpyArray[int], ...]:
"""Return a tuple of length n_cells of each cell's point indices."""
cells = _get_connectivity_array(cellarr)
if len(cells) == 0:
return ()
offsets = _get_offset_array(cellarr)
return tuple(np.split(cells, offsets[1:-1]))
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,215 @@
"""PyVista specific errors."""
from __future__ import annotations
class NotAllTrianglesError(ValueError):
"""Exception when a mesh does not contain all triangles.
Parameters
----------
message : str
Error message.
"""
def __init__(self, message='Mesh must consist of only triangles') -> None:
"""Empty init."""
ValueError.__init__(self, message)
class DeprecationError(RuntimeError):
"""Used for deprecated methods and functions.
Parameters
----------
message : str
Error message.
"""
def __init__(self, message='This feature has been deprecated') -> None:
"""Empty init."""
RuntimeError.__init__(self, message)
class VTKVersionError(RuntimeError):
"""Requested feature is not supported by the installed VTK version.
Parameters
----------
message : str
Error message.
"""
def __init__(
self,
message='The requested feature is not supported by the installed VTK version.',
) -> None: # numpydoc ignore=PR01,RT01
"""Empty init."""
RuntimeError.__init__(self, message)
class PointSetNotSupported(TypeError): # noqa: N818
"""Requested filter or property is not supported by the PointSet class.
Parameters
----------
message : str
Error message.
"""
def __init__(
self,
message='The requested operation is not supported for PointSets.',
) -> None: # numpydoc ignore=PR01,RT01
"""Empty init."""
TypeError.__init__(self, message)
class PointSetCellOperationError(PointSetNotSupported):
"""Requested filter or property is not supported by the PointSet class.
Parameters
----------
message : str
Error message.
"""
def __init__(
self,
message='Cell operations are not supported. PointSets contain no cells.',
) -> None: # numpydoc ignore=PR01,RT01
"""Empty init."""
PointSetNotSupported.__init__(self, message)
class PointSetDimensionReductionError(PointSetNotSupported):
"""Requested filter or property is not supported by the PointSet class.
Parameters
----------
message : str
Error message.
"""
def __init__(
self,
message='Slice and other dimension reducing filters are not supported on PointSets.',
) -> None: # numpydoc ignore=PR01,RT01
"""Empty init."""
PointSetNotSupported.__init__(self, message)
class PartitionedDataSetsNotSupported(TypeError): # noqa: N818
"""Requested filter or property is not supported by the PartitionedDataSets class.
Parameters
----------
message : str
Error message.
"""
def __init__(
self,
message='The requested operation is not supported for PartitionedDataSetss.',
) -> None: # numpydoc ignore=PR01,RT01
"""Empty init."""
TypeError.__init__(self, message)
class MissingDataError(ValueError):
"""Exception when data is missing, e.g. no active scalars can be set.
Parameters
----------
message : str
Error message.
"""
def __init__(self, message='No data available.') -> None:
"""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.
Parameters
----------
message : str
Error message.
"""
def __init__(self, message='Multiple data available.') -> None:
"""Call the base class constructor with the custom message."""
super().__init__(message)
class CellSizeError(ValueError):
"""Exception when a cell array size is invalid.
Parameters
----------
message : str
Error message.
"""
def __init__(self, message='Cell array size is invalid.') -> None:
"""Call the base class constructor with the custom message."""
super().__init__(message)
class PyVistaPipelineError(RuntimeError):
"""Exception when a VTK pipeline runs into an issue.
Parameters
----------
message : str
Error message.
"""
def __init__(
self,
message='VTK pipeline issue detected by PyVista.',
) -> None: # numpydoc ignore=PR01,RT01
"""Call the base class constructor with the custom message."""
super().__init__(message)
class PyVistaAttributeError(AttributeError):
"""Exception when accessing an attribute that is not part of the PyVista API.
Parameters
----------
message : str
Error message.
"""
def __init__(
self,
message='The attribute is not part of the PyVista API',
) -> None: # numpydoc ignore=PR01,RT01
super().__init__(message)
class PyVistaDeprecationWarning(Warning):
"""Non-supressed Deprecation Warning."""
class PyVistaFutureWarning(Warning):
"""Non-supressed Future Warning."""
class PyVistaEfficiencyWarning(Warning):
"""Efficiency warning."""
@@ -0,0 +1,95 @@
"""These classes hold methods to apply general filters to any data type.
By inheriting these classes into the wrapped VTK data structures, a user
can easily apply common filters in an intuitive manner.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> dataset = examples.load_uniform()
>>> # Threshold
>>> thresh = dataset.threshold([100, 500])
>>> # Slice
>>> slc = dataset.slice()
>>> # Clip
>>> clp = dataset.clip(invert=True)
>>> # Contour
>>> iso = dataset.contour()
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import cast
import pyvista
from pyvista.core.utilities.helpers import wrap
from pyvista.core.utilities.observers import ProgressMonitor
if TYPE_CHECKING:
from pyvista.core import _vtk_core as _vtk
def _update_alg(alg, *, progress_bar: bool = False, message='') -> None:
"""Update an algorithm with or without a progress bar."""
if progress_bar:
with ProgressMonitor(alg, message=message):
alg.Update()
else:
alg.Update()
def _get_output(
algorithm: _vtk.vtkAlgorithm,
*,
iport=0,
iconnection=0,
oport=0,
active_scalars=None,
active_scalars_field='point',
):
"""Get the algorithm's output and copy input's pyvista meta info."""
ido = cast('pyvista.DataObject', wrap(algorithm.GetInputDataObject(iport, iconnection)))
data = cast('pyvista.DataObject', wrap(algorithm.GetOutputDataObject(oport)))
if not isinstance(data, pyvista.MultiBlock):
data.copy_meta_from(ido, deep=True)
if not data.field_data and ido.field_data:
data.field_data.update(ido.field_data)
if active_scalars is not None:
data.set_active_scalars(active_scalars, preference=active_scalars_field)
# return a PointSet if input is a pointset
if isinstance(ido, pyvista.PointSet):
return data.cast_to_pointset()
return data
from .composite import CompositeFilters
from .data_object import DataObjectFilters
# Re-export submodules to maintain the same import paths
# before filters.py was split into submodules
from .data_set import DataSetFilters
from .image_data import ImageDataFilters
from .poly_data import PolyDataFilters
from .rectilinear_grid import RectilinearGridFilters
from .structured_grid import StructuredGridFilters
from .unstructured_grid import UnstructuredGridFilters
__all__ = [
'CompositeFilters',
'DataObjectFilters',
'DataSetFilters',
'ImageDataFilters',
'PolyDataFilters',
'RectilinearGridFilters',
'StructuredGridFilters',
'UnstructuredGridFilters',
'_get_output',
'_update_alg',
]
@@ -0,0 +1,408 @@
"""Filters module with a class to manage filters/algorithms for composite datasets."""
from __future__ import annotations
import functools
from typing import TYPE_CHECKING
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.filters import _get_output
from pyvista.core.filters import _update_alg
from pyvista.core.filters.data_object import DataObjectFilters
from pyvista.core.filters.data_set import DataSetFilters
from pyvista.core.utilities.helpers import wrap
from pyvista.core.utilities.misc import abstract_class
if TYPE_CHECKING:
from typing import Callable
from pyvista import MultiBlock
from pyvista.core.composite import _TypeMultiBlockLeaf
@abstract_class
class CompositeFilters(DataObjectFilters):
"""An internal class to manage filters/algorithms for composite datasets."""
def generic_filter( # type:ignore[misc]
self: MultiBlock,
function: str | Callable[..., _TypeMultiBlockLeaf],
/,
*args,
**kwargs,
) -> MultiBlock:
"""Apply any filter to all nested blocks recursively.
This filter applies a user-specified function or method to all blocks in
this :class:`~pyvista.MultiBlock`.
.. note::
If an ``inplace`` keyword is used, this ``MultiBlock`` is modified
in-place along with all blocks.
.. note::
By default, the specified ``function`` is not applied to any ``None``
blocks. These are simply skipped and passed through to the output.
For advanced use, it is possible to apply the filter to ``None`` blocks
by using the undocumented keyword ``_skip_none=False``.
.. versionadded:: 0.45
Parameters
----------
function : Callable | str
Callable function or name of the method to apply to each block. The function
should accept a :class:`~pyvista.DataSet` as input and return either a
:class:`~pyvista.DataSet` or :class:`~pyvista.MultiBlock` as output.
*args : Any, optional
Arguments to use with the specified ``function``.
**kwargs : Any, optional
Keyword arguments to use with the specified ``function``.
Returns
-------
MultiBlock
Filtered dataset.
Raises
------
RuntimeError
Raised if the filter cannot be applied to any block for any reason. This
overrides ``TypeError``, ``ValueError``, ``AttributeError`` errors when
filtering.
See Also
--------
pyvista.MultiBlock.flatten
pyvista.MultiBlock.recursive_iterator
pyvista.MultiBlock.clean
Examples
--------
Create a :class:`~pyvista.MultiBlock` with various mesh types.
>>> import pyvista as pv
>>> from pyvista import examples
>>> import numpy as np
>>> volume = examples.load_uniform()
>>> poly = examples.load_ant()
>>> unstructured = examples.load_tetbeam()
>>> multi = pv.MultiBlock([volume, poly, unstructured])
>>> [type(block) for block in multi] # doctest: +NORMALIZE_WHITESPACE
[<class 'pyvista.core.grid.ImageData'>,
<class 'pyvista.core.pointset.PolyData'>,
<class 'pyvista.core.pointset.UnstructuredGrid'>]
Use the generic filter to apply :meth:`~pyvista.DataSet.cast_to_unstructured_grid`
to all blocks.
>>> filtered = multi.generic_filter('cast_to_unstructured_grid')
>>> [type(block) for block in filtered] # doctest: +NORMALIZE_WHITESPACE
[<class 'pyvista.core.pointset.UnstructuredGrid'>,
<class 'pyvista.core.pointset.UnstructuredGrid'>,
<class 'pyvista.core.pointset.UnstructuredGrid'>]
Use the :meth:`~pyvista.DataSetFilters.partition` filter on all blocks.
Any arguments can be specified as though the filter is being used directly.
>>> filtered = multi.generic_filter('partition', 4, as_composite=True)
Any function can be used as long as it returns a :class:`~pyvista.DataSet` or
:class:`~pyvista.MultiBlock`. For example, we can normalize each block
independently to have bounds between ``-0.5`` and ``0.5``.
>>> def normalize_bounds(dataset):
... # Center the dataset
... dataset = dataset.translate(-np.array(dataset.center))
... # Scale the dataset
... factor = 1 / np.array(dataset.bounds_size)
... return dataset.scale(factor)
>>> filtered = multi.generic_filter(normalize_bounds)
>>> filtered
MultiBlock (...)
N Blocks: 3
X Bounds: -5.000e-01, 5.000e-01
Y Bounds: -5.000e-01, 5.000e-01
Z Bounds: -5.000e-01, 5.000e-01
The generic filter will fail if the filter can only be applied to some blocks
but not others. For example, it is not possible to use the
:meth:`~pyvista.ImageDataFilters.resample` filter generically since the
``MultiBlock`` above is heterogeneous and contains some blocks which are not
:class:`~pyvista.ImageData`.
>>> multi.generic_filter('resample', 0.5) # doctest:+SKIP
RuntimeError: The filter 'resample' could not be applied to the block at index 1 with
name 'Block-01' and type PolyData.
Use a custom function instead to apply the generic filter conditionally. Here we
filter the image blocks but simply pass-through a copy of any other blocks.
>>> def conditional_resample(dataset, *args, **kwargs):
... if isinstance(dataset, pv.ImageData):
... return dataset.resample(*args, **kwargs)
... return dataset.copy()
>>> filtered = multi.generic_filter(conditional_resample, 0.5)
"""
# Set default undocumented kwargs. A function is used here to prevent IDEs from
# suggesting these keywords to users.
def get_iterator_kwargs(kwargs_) -> tuple[bool, bool]:
# Skip None blocks by default
skip_none_: bool = kwargs_.pop('_skip_none', True)
# Do not skip empty blocks by default
skip_empty_: bool = kwargs_.pop('_skip_empty', False)
return skip_none_, skip_empty_
skip_none, skip_empty = get_iterator_kwargs(kwargs)
def apply_filter(function_, ids_, name_, block_): # noqa: PLR0917
try:
function_ = (
getattr(block_, function_)
if isinstance(function_, str)
else functools.partial(function_, block_)
)
output_ = function_(**kwargs) if len(args) == 0 else function_(*args, **kwargs)
except (AttributeError, ValueError, TypeError, RuntimeError) as e:
# Construct a helpful error message
func_name = (
function_.func if isinstance(function_, functools.partial) else function_
)
obj_name = type(block).__name__
if len(ids_) == 1:
index = ids_[0]
nested = ' '
else:
nested = ' nested '
index = _format_nested_index(ids)
msg = (
f"The filter '{func_name}'\n"
f'could not be applied to the{nested}block at index {index} with '
f"name '{name_}' and type {obj_name}."
)
raise RuntimeError(msg) from e
return output_
def get_iterator(multi, skip_none_, skip_empty_):
return multi.recursive_iterator(
'all', skip_none=skip_none_, skip_empty=skip_empty_, nested_ids=True
)
# Apply filter in-place
inplace = kwargs.get('inplace')
if inplace:
for ids, name, block in get_iterator(self, skip_none, skip_empty):
apply_filter(function, ids, name, block)
return self
# Create a copy and replace all the blocks
output = pyvista.MultiBlock()
output.shallow_copy(self, recursive=True)
for ids, name, block in get_iterator(output, skip_none, skip_empty):
filtered = apply_filter(function, ids, name, block)
# Only replace if necessary
if filtered is not block:
output.replace(ids, filtered)
return output
def extract_geometry(self):
"""Extract the surface the geometry of all blocks.
Place this filter at the end of a pipeline before a polydata
consumer such as a polydata mapper to extract geometry from
all blocks and append them to one polydata object.
Returns
-------
pyvista.PolyData
Surface of the composite dataset.
"""
gf = _vtk.vtkCompositeDataGeometryFilter()
gf.SetInputData(self)
gf.Update()
return wrap(gf.GetOutputDataObject(0))
@_deprecate_positional_args
def combine(self, merge_points: bool = False, tolerance=0.0): # noqa: FBT001, FBT002
"""Combine all blocks into a single unstructured grid.
Parameters
----------
merge_points : bool, default: False
Merge coincidental points.
tolerance : float, default: 0.0
The absolute tolerance to use to find coincident points when
``merge_points=True``.
Returns
-------
pyvista.UnstructuredGrid
Combined blocks.
Examples
--------
Combine blocks within a multiblock without merging points.
>>> import pyvista as pv
>>> block = pv.MultiBlock(
... [
... pv.Cube(clean=False),
... pv.Cube(center=(1, 0, 0), clean=False),
... ]
... )
>>> merged = block.combine()
>>> merged.n_points
48
Combine blocks and merge points
>>> merged = block.combine(merge_points=True)
>>> merged.n_points
12
"""
alg = _vtk.vtkAppendFilter()
for block in self: # type: ignore[attr-defined]
single_block = (
CompositeFilters.combine(
block, # type: ignore[arg-type]
merge_points=merge_points,
tolerance=tolerance,
)
if isinstance(block, _vtk.vtkMultiBlockDataSet)
else block
)
alg.AddInputData(single_block)
alg.SetMergePoints(merge_points)
alg.SetTolerance(tolerance)
alg.Update()
return wrap(alg.GetOutputDataObject(0))
@_deprecate_positional_args
def outline( # type: ignore[misc]
self: MultiBlock,
generate_faces: bool = False, # noqa: FBT001, FBT002
nested: bool = False, # noqa: FBT001, FBT002
progress_bar: bool = False, # noqa: FBT001, FBT002
):
"""Produce an outline of the full extent for the all blocks in this composite dataset.
Parameters
----------
generate_faces : bool, default: False
Generate solid faces for the box.
nested : bool, default: False
If ``True``, these creates individual outlines for each nested dataset.
progress_bar : bool, default: False
Display a progress bar to indicate progress.
Returns
-------
pyvista.PolyData
Mesh containing the outline.
"""
if nested:
return DataSetFilters.outline(
self,
generate_faces=generate_faces,
progress_bar=progress_bar,
)
box = pyvista.Box(bounds=self.bounds)
return box.outline(generate_faces=generate_faces, progress_bar=progress_bar)
@_deprecate_positional_args
def outline_corners( # type: ignore[misc]
self: MultiBlock,
factor=0.2,
nested: bool = False, # noqa: FBT001, FBT002
progress_bar: bool = False, # noqa: FBT001, FBT002
):
"""Produce an outline of the corners for the all blocks in this composite dataset.
Parameters
----------
factor : float, default: 0.2
Controls the relative size of the corners to the length of
the corresponding bounds.
nested : bool, default: False
If ``True``, these creates individual outlines for each nested dataset.
progress_bar : bool, default: False
Display a progress bar to indicate progress.
Returns
-------
pyvista.PolyData
Mesh containing outlined corners.
"""
if nested:
return DataSetFilters.outline_corners(self, factor=factor, progress_bar=progress_bar)
box = pyvista.Box(bounds=self.bounds)
return box.outline_corners(factor=factor, progress_bar=progress_bar)
@_deprecate_positional_args
def _compute_normals( # noqa: PLR0917
self,
cell_normals: bool = True, # noqa: FBT001, FBT002
point_normals: bool = True, # noqa: FBT001, FBT002
split_vertices: bool = False, # noqa: FBT001, FBT002
flip_normals: bool = False, # noqa: FBT001, FBT002
consistent_normals: bool = True, # noqa: FBT001, FBT002
auto_orient_normals: bool = False, # noqa: FBT001, FBT002
non_manifold_traversal: bool = True, # noqa: FBT001, FBT002
feature_angle=30.0,
track_vertices: bool = False, # noqa: FBT001, FBT002
progress_bar: bool = False, # noqa: FBT001, FBT002
):
"""Compute point and/or cell normals for a multi-block dataset."""
if not self.is_all_polydata: # type: ignore[attr-defined]
msg = (
'This multiblock contains non-PolyData datasets. Convert all the '
'datasets to PolyData with `as_polydata`'
)
raise RuntimeError(msg)
# track original point indices
if split_vertices and track_vertices:
for block in self: # type: ignore[attr-defined]
ids = np.arange(block.n_points, dtype=pyvista.ID_TYPE)
block.point_data.set_array(ids, 'pyvistaOriginalPointIds')
alg = _vtk.vtkPolyDataNormals()
alg.SetComputeCellNormals(cell_normals)
alg.SetComputePointNormals(point_normals)
alg.SetSplitting(split_vertices)
alg.SetFlipNormals(flip_normals)
alg.SetConsistency(consistent_normals)
alg.SetAutoOrientNormals(auto_orient_normals)
alg.SetNonManifoldTraversal(non_manifold_traversal)
alg.SetFeatureAngle(feature_angle)
alg.SetInputData(self)
_update_alg(alg, progress_bar=progress_bar, message='Computing Normals')
return _get_output(alg)
def _format_nested_index(index: tuple[int, ...]) -> str:
return ''.join([f'[{ind}]' for ind in index])
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,149 @@
"""Filters module with the class to manage filters/algorithms for rectilinear grid datasets."""
from __future__ import annotations
from collections.abc import Sequence
import numpy as np
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.filters import _get_output
from pyvista.core.filters import _update_alg
from pyvista.core.utilities.misc import abstract_class
@abstract_class
class RectilinearGridFilters:
"""An internal class to manage filters/algorithms for rectilinear grid datasets."""
@_deprecate_positional_args(allowed=['tetra_per_cell'])
def to_tetrahedra( # noqa: PLR0917
self,
tetra_per_cell: int = 5,
mixed: str | Sequence[int] | bool = False, # noqa: FBT001, FBT002
pass_cell_ids: bool = True, # noqa: FBT001, FBT002
pass_data: bool = True, # noqa: FBT001, FBT002
progress_bar: bool = False, # noqa: FBT001, FBT002
):
"""Create a tetrahedral mesh structured grid.
Parameters
----------
tetra_per_cell : int, default: 5
The number of tetrahedrons to divide each cell into. Can be
either ``5``, ``6``, or ``12``. If ``mixed=True``, this value is
overridden.
mixed : str, bool, sequence, default: False
When set, subdivides some cells into 5 and some cells into 12. Set
to ``True`` to use the active cell scalars of the
:class:`pyvista.RectilinearGrid` to be either 5 or 12 to
determining the number of tetrahedra to generate per cell.
When a sequence, uses these values to subdivide the cells. When a
string uses a cell array rather than the active array to determine
the number of tetrahedra to generate per cell.
pass_cell_ids : bool, default: True
Set to ``True`` to make the tetrahedra have scalar data indicating
which cell they came from in the original
:class:`pyvista.RectilinearGrid`. The name of this array is
``'vtkOriginalCellIds'`` within the ``cell_data``.
pass_data : bool, default: True
Set to ``True`` to make the tetrahedra mesh have the cell data from
the original :class:`pyvista.RectilinearGrid`. This uses
``pass_cell_ids=True`` internally. If ``True``, ``pass_cell_ids``
will also be set to ``True``.
progress_bar : bool, default: False
Display a progress bar to indicate progress.
Returns
-------
pyvista.UnstructuredGrid
UnstructuredGrid containing the tetrahedral cells.
Examples
--------
Divide a rectangular grid into tetrahedrons. Each cell contains by
default 5 tetrahedrons.
First, create and plot the grid.
>>> import numpy as np
>>> import pyvista as pv
>>> xrng = np.linspace(0, 1, 2)
>>> yrng = np.linspace(0, 1, 2)
>>> zrng = np.linspace(0, 2, 3)
>>> grid = pv.RectilinearGrid(xrng, yrng, zrng)
>>> grid.plot()
Now, generate the tetrahedra plot in the exploded view of the cell.
>>> tet_grid = grid.to_tetrahedra()
>>> tet_grid.explode(factor=0.5).plot(show_edges=True)
Take the same grid but divide the first cell into 5 cells and the other
cell into 12 tetrahedrons per cell.
>>> tet_grid = grid.to_tetrahedra(mixed=[5, 12])
>>> tet_grid.explode(factor=0.5).plot(show_edges=True)
"""
alg = _vtk.vtkRectilinearGridToTetrahedra()
alg.SetRememberVoxelId(pass_cell_ids or pass_data)
if mixed is not False:
if isinstance(mixed, str):
self.cell_data.active_scalars_name = mixed # type: ignore[attr-defined]
elif isinstance(mixed, (np.ndarray, Sequence)):
self.cell_data['_MIXED_CELLS_'] = mixed # type: ignore[attr-defined]
elif not isinstance(mixed, bool):
msg = '`mixed` must be either a sequence of ints or bool' # type: ignore[unreachable]
raise TypeError(msg)
alg.SetTetraPerCellTo5And12()
else:
if tetra_per_cell not in [5, 6, 12]:
msg = f'`tetra_per_cell` should be either 5, 6, or 12, not {tetra_per_cell}'
raise ValueError(msg)
# Edge case causing a seg-fault where grid is flat in one dimension
# See: https://gitlab.kitware.com/vtk/vtk/-/issues/18650
if 1 in self.dimensions and tetra_per_cell == 12: # type: ignore[attr-defined]
msg = (
'Cannot split cells into 12 tetrahedrals when at least '
f'one dimension is 1. Dimensions are {self.dimensions}.' # type: ignore[attr-defined]
)
raise RuntimeError(msg)
alg.SetTetraPerCell(tetra_per_cell)
alg.SetInputData(self)
_update_alg(alg, progress_bar=progress_bar, message='Converting to tetrahedra')
out = _get_output(alg)
if pass_data:
# algorithm stores original cell ids in active scalars
# this does not preserve active scalars, but we need to
# keep active scalars until they are renamed
for name in self.cell_data: # type: ignore[attr-defined]
if name != out.cell_data.active_scalars_name:
out[name] = self.cell_data[name][out.cell_data.active_scalars] # type: ignore[attr-defined]
for name in self.point_data: # type: ignore[attr-defined]
out[name] = self.point_data[name] # type: ignore[attr-defined]
if alg.GetRememberVoxelId():
# original cell_ids are not named and are the active scalars
out.cell_data.set_array(
out.cell_data.pop(out.cell_data.active_scalars_name),
'vtkOriginalCellIds',
)
if pass_data:
# Now reset active scalars in cast the original mesh had data with active scalars
association, name = self.active_scalars_info # type: ignore[attr-defined]
out.set_active_scalars(name, preference=association)
return out
@@ -0,0 +1,202 @@
"""Filters module with class to manage filters/algorithms for structured grid datasets."""
from __future__ import annotations
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.filters import _get_output
from pyvista.core.filters.data_set import DataSetFilters
from pyvista.core.utilities.misc import abstract_class
@abstract_class
class StructuredGridFilters(DataSetFilters):
"""An internal class to manage filters/algorithms for structured grid datasets."""
@_deprecate_positional_args(allowed=['voi', 'rate'])
def extract_subset(self, voi, rate=(1, 1, 1), boundary: bool = False): # noqa: FBT001, FBT002
"""Select piece (e.g., volume of interest).
To use this filter set the VOI ivar which are i-j-k min/max
indices that specify a rectangular region in the data. (Note
that these are 0-offset.) You can also specify a sampling rate
to subsample the data.
Typical applications of this filter are to extract a slice
from a volume for image processing, subsampling large volumes
to reduce data size, or extracting regions of a volume with
interesting data.
Parameters
----------
voi : sequence[int]
Length 6 iterable of ints: ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
These bounds specify the volume of interest in i-j-k min/max
indices.
rate : sequence[int], default: (1, 1, 1)
Length 3 iterable of ints: ``(xrate, yrate, zrate)``.
boundary : bool, default: False
Control whether to enforce that the "boundary" of the grid
is output in the subsampling process. (This only has
effect when the rate in any direction is not equal to
1). When this is on, the subsampling will always include
the boundary of the grid even if the sample rate is
not an even multiple of the grid dimensions.
Returns
-------
pyvista.StructuredGrid
StructuredGrid with extracted subset.
Examples
--------
Split a grid in half.
>>> import numpy as np
>>> import pyvista as pv
>>> from pyvista import examples
>>> grid = examples.load_structured()
>>> voi_1 = grid.extract_subset([0, 80, 0, 40, 0, 1], boundary=True)
>>> voi_2 = grid.extract_subset([0, 80, 40, 80, 0, 1], boundary=True)
For fun, add the two grids back together and show they are
identical to the original grid.
>>> joined = voi_1.concatenate(voi_2, axis=1)
>>> assert np.allclose(grid.points, joined.points)
"""
alg = _vtk.vtkExtractGrid()
alg.SetVOI(voi)
alg.SetInputDataObject(self)
alg.SetSampleRate(rate)
alg.SetIncludeBoundary(boundary)
alg.Update()
return _get_output(alg)
def concatenate(self, other, axis, tolerance=0.0):
"""Concatenate a structured grid to this grid.
Joins structured grids into a single structured grid. Grids
must be of compatible dimension, and must be coincident along
the seam. Grids must have the same point and cell data. Field
data is ignored.
Parameters
----------
other : pyvista.StructuredGrid
Structured grid to concatenate.
axis : int
Axis along which to concatenate.
tolerance : float, default: 0.0
Tolerance for point coincidence along joining seam.
Returns
-------
pyvista.StructuredGrid
Concatenated grid.
Examples
--------
Split a grid in half and join them.
>>> import numpy as np
>>> import pyvista as pv
>>> from pyvista import examples
>>> grid = examples.load_structured()
>>> voi_1 = grid.extract_subset([0, 80, 0, 40, 0, 1], boundary=True)
>>> voi_2 = grid.extract_subset([0, 80, 40, 80, 0, 1], boundary=True)
>>> joined = voi_1.concatenate(voi_2, axis=1)
>>> f'{grid.dimensions} same as {joined.dimensions}'
'(80, 80, 1) same as (80, 80, 1)'
"""
if axis > 2:
msg = 'Concatenation axis must be <= 2.'
raise RuntimeError(msg)
# check dimensions are compatible
for i, (dim1, dim2) in enumerate(zip(self.dimensions, other.dimensions)): # type: ignore[attr-defined]
if i == axis:
continue
if dim1 != dim2:
msg = (
f'StructuredGrids with dimensions {self.dimensions} and {other.dimensions} ' # type: ignore[attr-defined]
'are not compatible.'
)
raise ValueError(msg)
# check point/cell variables are the same
if set(self.point_data.keys()) != set(other.point_data.keys()): # type: ignore[attr-defined]
msg = 'Grid to concatenate has different point array names.'
raise RuntimeError(msg)
if set(self.cell_data.keys()) != set(other.cell_data.keys()): # type: ignore[attr-defined]
msg = 'Grid to concatenate has different cell array names.'
raise RuntimeError(msg)
# check that points are coincident (within tolerance) along seam
if not np.allclose(
np.take(self.points_matrix, indices=-1, axis=axis), # type: ignore[attr-defined]
np.take(other.points_matrix, indices=0, axis=axis),
atol=tolerance,
):
msg = (
f'Grids cannot be joined along axis {axis}, as points '
'are not coincident within tolerance of {tolerance}.'
)
raise RuntimeError(msg)
# slice to cut off the repeated grid face
slice_spec = [slice(None, None, None)] * 3
slice_spec[axis] = slice(0, -1, None)
slice_spec = tuple(slice_spec) # type: ignore[assignment] # trigger basic indexing
# concatenate points, cutting off duplicate
new_points = np.concatenate(
(self.points_matrix[slice_spec], other.points_matrix), # type: ignore[attr-defined]
axis=axis,
)
# concatenate point arrays, cutting off duplicate
new_point_data = {}
for name, point_array in self.point_data.items(): # type: ignore[attr-defined]
arr_1 = self._reshape_point_array(point_array) # type: ignore[attr-defined]
arr_2 = other._reshape_point_array(other.point_data[name])
if not np.array_equal(
np.take(arr_1, indices=-1, axis=axis),
np.take(arr_2, indices=0, axis=axis),
):
msg = (
f'Grids cannot be joined along axis {axis}, as field '
'`{name}` is not identical along the seam.'
)
raise RuntimeError(msg)
new_point_data[name] = np.concatenate((arr_1[slice_spec], arr_2), axis=axis).ravel(
order='F',
)
new_dims = np.array(self.dimensions) # type: ignore[attr-defined]
new_dims[axis] += other.dimensions[axis] - 1
# concatenate cell arrays
new_cell_data = {}
for name, cell_array in self.cell_data.items(): # type: ignore[attr-defined]
arr_1 = self._reshape_cell_array(cell_array) # type: ignore[attr-defined]
arr_2 = other._reshape_cell_array(other.cell_data[name])
new_cell_data[name] = np.concatenate((arr_1, arr_2), axis=axis).ravel(order='F')
# assemble output
joined = pyvista.StructuredGrid()
joined.dimensions = list(new_dims)
joined.points = new_points.reshape((-1, 3), order='F')
joined.point_data.update(new_point_data)
joined.cell_data.update(new_cell_data)
return joined
@@ -0,0 +1,250 @@
"""Filters module with a class to manage filters/algorithms for unstructured grid datasets."""
from __future__ import annotations
from functools import wraps
from typing import TYPE_CHECKING
import numpy as np
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.errors import VTKVersionError
from pyvista.core.filters import _get_output
from pyvista.core.filters import _update_alg
from pyvista.core.filters.data_set import DataSetFilters
from pyvista.core.filters.poly_data import PolyDataFilters
from pyvista.core.utilities.misc import abstract_class
if TYPE_CHECKING:
from pyvista.core._typing_core._dataset_types import _UnstructuredGridType
@abstract_class
class UnstructuredGridFilters(DataSetFilters):
"""An internal class to manage filters/algorithms for unstructured grid datasets."""
@wraps(PolyDataFilters.delaunay_2d) # type: ignore[has-type]
def delaunay_2d(self, *args, **kwargs): # numpydoc ignore=PR01,RT01
"""Wrap ``PolyDataFilters.delaunay_2d``."""
return PolyDataFilters.delaunay_2d(self, *args, **kwargs) # type: ignore[arg-type]
@wraps(PolyDataFilters.reconstruct_surface) # type: ignore[has-type]
def reconstruct_surface(self, *args, **kwargs): # numpydoc ignore=PR01,RT01
"""Wrap ``PolyDataFilters.reconstruct_surface``."""
return PolyDataFilters.reconstruct_surface(self, *args, **kwargs) # type: ignore[arg-type]
def subdivide_tetra(self):
"""Subdivide each tetrahedron into twelve tetrahedrons.
Returns
-------
pyvista.UnstructuredGrid
UnstructuredGrid containing the subdivided tetrahedrons.
Examples
--------
First, load a sample tetrahedral UnstructuredGrid and plot it.
>>> from pyvista import examples
>>> grid = examples.load_tetbeam()
>>> grid.plot(show_edges=True, line_width=2)
Now, subdivide and plot.
>>> subdivided = grid.subdivide_tetra()
>>> subdivided.plot(show_edges=True, line_width=2)
"""
alg = _vtk.vtkSubdivideTetra()
alg.SetInputData(self)
_update_alg(alg)
return _get_output(alg)
@_deprecate_positional_args
def clean( # noqa: PLR0917
self,
tolerance=0,
remove_unused_points: bool = True, # noqa: FBT001, FBT002
produce_merge_map: bool = True, # noqa: FBT001, FBT002
average_point_data: bool = True, # noqa: FBT001, FBT002
merging_array_name=None,
progress_bar: bool = False, # noqa: FBT001, FBT002
):
"""Merge duplicate points and remove unused points in an UnstructuredGrid.
This filter, merging coincident points as defined by a merging
tolerance and optionally removes unused points. The filter does not
modify the topology of the input dataset, nor change the types of
cells. It may however, renumber the cell connectivity ids.
This filter implements :vtk:`vtkStaticCleanUnstructuredGrid`.
Parameters
----------
tolerance : float, default: 0.0
The absolute point merging tolerance.
remove_unused_points : bool, default: True
Indicate whether points unused by any cell are removed from the
output. Note that when this is off, the filter can successfully
process datasets with no cells (and just points). If on in this
case, and there are no cells, the output will be empty.
produce_merge_map : bool, default: False
Indicate whether a merge map should be produced on output.
The merge map, if requested, maps each input point to its
output point id, or provides a value of -1 if the input point
is not used in the output. The merge map is associated with
the filter's output field data and is named ``"PointMergeMap"``.
average_point_data : bool, default: True
Indicate whether point coordinates and point data of merged points
are averaged. When ``True``, the data coordinates and attribute
values of all merged points are averaged. When ``False``, the point
coordinate and data of the single remaining merged point is
retained.
merging_array_name : str, optional
If a ``merging_array_name`` is specified and exists in the
``point_data``, then point merging will switch into a mode where
merged points must be both geometrically coincident and have
matching point data. When set, ``tolerance`` has no effect.
progress_bar : bool, default: False
Display a progress bar to indicate progress.
Returns
-------
UnstructuredGrid
Cleaned unstructured grid.
See Also
--------
remove_unused_points
Strictly remove unused points `without` merging points.
Examples
--------
Demonstrate cleaning an UnstructuredGrid and show how it can be used to
average the point data across merged points.
>>> import pyvista as pv
>>> from pyvista import examples
>>> hexbeam = examples.load_hexbeam()
>>> hexbeam_shifted = hexbeam.translate([1, 0, 0])
>>> hexbeam.point_data['data'] = [0] * hexbeam.n_points
>>> hexbeam_shifted.point_data['data'] = [1] * hexbeam.n_points
>>> merged = hexbeam.merge(hexbeam_shifted, merge_points=False)
>>> cleaned = merged.clean(average_point_data=True)
>>> cleaned.n_points < merged.n_points
True
Show how point averaging using the ``clean`` method with
``average_point_data=True`` results in averaged point data for merged
points.
>>> pl = pv.Plotter(shape=(1, 2))
>>> _ = pl.add_mesh(merged, scalars='data', show_scalar_bar=False)
>>> pl.subplot(0, 1)
>>> _ = pl.add_mesh(cleaned, scalars='data')
>>> pl.show()
"""
try:
from vtkmodules.vtkFiltersCore import vtkStaticCleanUnstructuredGrid # noqa: PLC0415
except ImportError: # pragma no cover
msg = 'UnstructuredGrid.clean requires VTK >= 9.2.2'
raise VTKVersionError(msg) from None
alg = vtkStaticCleanUnstructuredGrid()
# https://github.com/pyvista/pyvista/pull/6337
alg.SetInputDataObject(self.copy()) # type: ignore[attr-defined]
alg.SetAbsoluteTolerance(True)
alg.SetTolerance(tolerance)
alg.SetMergingArray(merging_array_name)
alg.SetRemoveUnusedPoints(remove_unused_points)
alg.SetProduceMergeMap(produce_merge_map)
alg.SetAveragePointData(average_point_data)
_update_alg(alg, progress_bar=progress_bar, message='Cleaning Unstructured Grid')
return _get_output(alg)
def remove_unused_points( # type: ignore[misc]
self: _UnstructuredGridType,
*,
inplace: bool = False,
) -> _UnstructuredGridType:
"""Remove points which are not used by any cells.
Unlike :meth:`clean`, this filter does `not` merge points.
.. versionadded:: 0.46
Parameters
----------
inplace : bool, default: False
If ``True`` the mesh is updated in-place, otherwise a copy is returned.
See Also
--------
pyvista.PolyDataFilters.remove_unused_points
Returns
-------
UnstructuredGrid
Mesh with unused points removed.
Examples
--------
Create :class:`~pyvista.UnstructuredGrid` with three points. The first two points are
coincident and associated with :attr:`~pyvista.CellType.VERTEX` cells, and the third point
is "unused" and not associated with any cells.
>>> import pyvista as pv
>>> cells = [1, 0, 1, 1]
>>> celltypes = [pv.CellType.VERTEX, pv.CellType.VERTEX]
>>> points = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]
>>> grid = pv.UnstructuredGrid(cells, celltypes, points)
>>> grid
UnstructuredGrid (...)
N Cells: 2
N Points: 3
X Bounds: 0.000e+00, 1.000e+00
Y Bounds: 0.000e+00, 1.000e+00
Z Bounds: 0.000e+00, 1.000e+00
N Arrays: 0
Since the third point is unused, we can remove it. Note that coincident points are `not`
merged by this filter, so the two vertex points are kept as-is.
>>> grid = grid.remove_unused_points()
>>> grid
UnstructuredGrid (...)
N Cells: 2
N Points: 2
X Bounds: 0.000e+00, 0.000e+00
Y Bounds: 0.000e+00, 0.000e+00
Z Bounds: 0.000e+00, 0.000e+00
N Arrays: 0
"""
if self.is_empty:
return self if inplace else self.copy()
out = self.copy()
# Need to add an extra "dummy" cell to force vtkExtractCells to remap the point IDs
cell_array = out.GetCells()
cell_array.InsertNextCell(1)
# Extract all the cells, except for the dummy cell
out = out.extract_cells(np.arange(self.n_cells))
if (name := 'vtkOriginalPointIds') in (data := out.point_data):
del data[name]
if (name := 'vtkOriginalCellIds') in (data := out.cell_data):
del data[name]
if inplace:
self.copy_from(out)
return self
return out
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,407 @@
"""Wrappers for :vtk:`vtkDataObject`.
The data objects does not have any sort of spatial reference.
"""
from __future__ import annotations
import numpy as np
import pyvista
from . import _vtk_core as _vtk
from .dataobject import DataObject
from .datasetattributes import DataSetAttributes
from .utilities.arrays import FieldAssociation
from .utilities.arrays import FieldLiteral
from .utilities.arrays import RowLiteral
from .utilities.arrays import get_array
from .utilities.arrays import row_array
class Table(DataObject, _vtk.vtkTable):
"""Wrapper for the :vtk:`vtkTable` class.
Create by passing a 2D NumPy array of shape (``n_rows`` by ``n_columns``)
or from a dictionary containing NumPy arrays.
Examples
--------
>>> import pyvista as pv
>>> import numpy as np
>>> arrays = np.random.default_rng().random((100, 3))
>>> table = pv.Table(arrays)
"""
def __init__(self, *args, deep: bool = True, **kwargs): # noqa: ARG002
"""Initialize the table."""
super().__init__()
if len(args) == 1:
if isinstance(args[0], _vtk.vtkTable):
if deep:
self.deep_copy(args[0])
else:
self.shallow_copy(args[0])
elif isinstance(args[0], (np.ndarray, list)):
self._from_arrays(args[0])
elif isinstance(args[0], dict):
self._from_dict(args[0])
elif 'pandas.core.frame.DataFrame' in str(type(args[0])):
self._from_pandas(args[0])
else:
msg = f'Table unable to be made from ({type(args[0])})'
raise TypeError(msg)
@staticmethod
def _prepare_arrays(arrays):
arrays = np.asarray(arrays)
if arrays.ndim == 1:
return np.reshape(arrays, (1, -1))
elif arrays.ndim == 2:
return arrays.T
else:
msg = 'Only 1D or 2D arrays are supported by Tables.'
raise ValueError(msg)
def _from_arrays(self, arrays) -> None:
np_table = self._prepare_arrays(arrays)
for i, array in enumerate(np_table):
self.row_arrays[f'Array {i}'] = array
def _from_dict(self, array_dict):
for array in array_dict.values():
if not isinstance(array, np.ndarray) and array.ndim < 3:
msg = 'Dictionary must contain only NumPy arrays with maximum of 2D.'
raise ValueError(msg)
for name, array in array_dict.items():
self.row_arrays[name] = array
def _from_pandas(self, data_frame) -> None:
for name in data_frame.keys():
self.row_arrays[name] = data_frame[name].values
@property
def n_rows(self):
"""Return the number of rows.
Returns
-------
int
The number of rows.
"""
return self.GetNumberOfRows()
@n_rows.setter
def n_rows(self, n) -> None:
"""Set the number of rows.
Parameters
----------
n : int
The number of rows.
"""
self.SetNumberOfRows(n)
@property
def n_columns(self):
"""Return the number of columns.
Returns
-------
int
The number of columns.
"""
return self.GetNumberOfColumns()
@property
def n_arrays(self):
"""Return the number of columns.
Alias for: ``n_columns``.
Returns
-------
int
The number of columns.
"""
return self.n_columns
def _row_array(self, name=None):
"""Return row scalars of a vtk object.
Parameters
----------
name : str
Name of row scalars to retrieve.
Returns
-------
numpy.ndarray
Numpy array of the row.
"""
return self.row_arrays.get_array(name)
@property
def row_arrays(self):
"""Return the all row arrays.
Returns
-------
int
The all row arrays.
"""
return DataSetAttributes(
vtkobject=self.GetRowData(),
dataset=self, # type: ignore[arg-type]
association=FieldAssociation.ROW,
)
def keys(self):
"""Return the table keys.
Returns
-------
list
List of the array names of this table.
"""
return self.row_arrays.keys()
def items(self):
"""Return the table items.
Returns
-------
list
List containing tuples pairs of the name and array of the table arrays.
"""
return self.row_arrays.items()
def values(self):
"""Return the table values.
Returns
-------
list
List of the table arrays.
"""
return self.row_arrays.values()
def update(self, data) -> None:
"""Set the table data using a dict-like update.
Parameters
----------
data : DataSetAttributes
Other dataset attributes to update from.
"""
if isinstance(data, (np.ndarray, list)):
# Allow table updates using array data
data = self._prepare_arrays(data)
data = {f'Array {i}': array for i, array in enumerate(data)}
self.row_arrays.update(data)
self.Modified()
def pop(self, name):
"""Pop off an array by the specified name.
Parameters
----------
name : int or str
Index or name of the row array.
Returns
-------
pyvista.pyvista_ndarray
PyVista array.
"""
return self.row_arrays.pop(name)
def __getitem__(self, index):
"""Search row data for an array."""
return self._row_array(name=index)
def _ipython_key_completions_(self):
return self.keys()
def get(self, index):
"""Get an array by its name.
Parameters
----------
index : int or str
Index or name of the row.
Returns
-------
pyvista.pyvista_ndarray
PyVista array.
"""
return self[index]
def __setitem__(self, name, scalars) -> None:
"""Add/set an array in the row_arrays."""
self.row_arrays[name] = scalars
def _remove_array(self, _, key) -> None:
"""Remove a single array by name from each field (internal helper)."""
self.row_arrays.remove(key)
def __delitem__(self, name) -> None:
"""Remove an array by the specified name."""
del self.row_arrays[name]
def __iter__(self):
"""Return the iterator across all arrays."""
for array_name in self.row_arrays:
yield self.row_arrays[array_name]
def _get_attrs(self):
"""Return the representation methods."""
attrs = []
attrs.append(('N Rows', self.n_rows, '{}'))
return attrs
def _repr_html_(self):
"""Return a pretty representation for Jupyter notebooks.
It includes header details and information about all arrays.
"""
fmt = ''
if self.n_arrays > 0:
fmt += "<table style='width: 100%;'>"
fmt += '<tr><th>Header</th><th>Data Arrays</th></tr>'
fmt += '<tr><td>'
# Get the header info
fmt += self.head(display=False, html=True)
# Fill out scalars arrays
if self.n_arrays > 0:
fmt += '</td><td>'
fmt += '\n'
fmt += "<table style='width: 100%;'>\n"
titles = ['Name', 'Type', 'N Comp', 'Min', 'Max']
fmt += '<tr>' + ''.join([f'<th>{t}</th>' for t in titles]) + '</tr>\n'
row = '<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n'
row = '<tr>' + ''.join(['<td>{}</td>' for i in range(len(titles))]) + '</tr>\n'
def format_array(key):
"""Format array information for printing (internal helper)."""
arr = row_array(self, key)
dl, dh = self.get_data_range(key)
dl = pyvista.FLOAT_FORMAT.format(dl) # type: ignore[assignment]
dh = pyvista.FLOAT_FORMAT.format(dh) # type: ignore[assignment]
ncomp = 0 if arr is None else arr.shape[1] if arr.ndim > 1 else 1
dtype = None if arr is None else arr.dtype
return row.format(key, dtype, ncomp, dl, dh)
for i in range(self.n_arrays):
key = self.GetRowData().GetArrayName(i)
fmt += format_array(key)
fmt += '</table>\n'
fmt += '\n'
fmt += '</td></tr> </table>'
return fmt
def __repr__(self):
"""Return the object representation."""
return self.head(display=False, html=False)
def __str__(self):
"""Return the object string representation."""
return self.head(display=False, html=False)
def to_pandas(self):
"""Create a Pandas DataFrame from this Table.
Returns
-------
pandas.DataFrame
This table represented as a pandas dataframe.
"""
try:
import pandas as pd # noqa: PLC0415
except ImportError: # pragma: no cover
msg = 'Install ``pandas`` to use this feature.'
raise ImportError(msg)
data_frame = pd.DataFrame()
for name, array in self.items():
data_frame[name] = array
return data_frame
def save(self, *args, **kwargs): # pragma: no cover
"""Save the table."""
msg = "Please use the `to_pandas` method and harness Pandas' wonderful file IO methods."
raise NotImplementedError(msg)
def get_data_range( # type: ignore[override]
self,
arr: str | None = None,
preference: FieldLiteral | RowLiteral = 'row',
) -> tuple[float, float]:
"""Get the min and max of a named array.
Parameters
----------
arr : str, numpy.ndarray, optional
The name of the array to get the range. If ``None``, the active scalar
is used.
preference : str, optional
When scalars is specified, this is the preferred array type
to search for in the dataset. Must be either ``'row'`` or
``'field'``.
Returns
-------
tuple
``(min, max)`` of the array.
"""
if arr is None:
# use the first array in the row data
arr = self.GetRowData().GetArrayName(0)
if isinstance(arr, str):
arr = get_array(self, arr, preference=preference) # type: ignore[assignment]
# If array has no tuples return a NaN range
if arr.size == 0 or not np.issubdtype(arr.dtype, np.number): # type: ignore[attr-defined]
return (np.nan, np.nan)
# Use the array range
return np.nanmin(arr), np.nanmax(arr)
@property
def is_empty(self) -> bool: # numpydoc ignore=RT01
"""Return ``True`` if the table has no rows and no columns.
.. versionadded:: 0.45
Examples
--------
>>> import pyvista as pv
>>> import numpy as np
>>> table = pv.Table()
>>> table.is_empty
True
>>> arrays = np.random.default_rng().random((100, 3))
>>> table = pv.Table(arrays)
>>> table.is_empty
False
"""
return self.n_rows == 0 and self.n_columns == 0
@@ -0,0 +1,287 @@
"""Contains the PartitionedDataSet class."""
from __future__ import annotations
from collections.abc import MutableSequence
from typing import TYPE_CHECKING
from typing import overload
from pyvista._deprecate_positional_args import _deprecate_positional_args
from . import _vtk_core as _vtk
from .dataobject import DataObject
from .errors import PartitionedDataSetsNotSupported
from .utilities.helpers import is_pyvista_dataset
from .utilities.helpers import wrap
if TYPE_CHECKING:
from collections.abc import Iterable
from typing_extensions import Self
from .dataset import DataSet
from .utilities.arrays import FieldAssociation
class PartitionedDataSet(DataObject, MutableSequence, _vtk.vtkPartitionedDataSet): # type: ignore[type-arg]
"""Wrapper for the :vtk:`vtkPartitionedDataSet` class.
DataSet which composite dataset to encapsulates a dataset consisting of partitions.
Examples
--------
>>> import pyvista as pv
>>> data = [
... pv.Sphere(center=(2, 0, 0)),
... pv.Cube(center=(0, 2, 0)),
... pv.Cone(),
... ]
>>> partitions = pv.PartitionedDataSet(data)
>>> len(partitions)
3
"""
if _vtk.vtk_version_info >= (9, 1):
_WRITERS = {'.vtpd': _vtk.vtkXMLPartitionedDataSetWriter}
if _vtk.vtk_version_info >= (9, 4):
_WRITERS['.vtkhdf'] = _vtk.vtkHDFWriter
def __init__(self, *args, **kwargs):
"""Initialize the PartitionedDataSet."""
super().__init__()
if len(args) == 1:
if isinstance(args[0], _vtk.vtkPartitionedDataSet):
deep = kwargs.get('deep', True)
if deep:
self.deep_copy(args[0])
else:
raise PartitionedDataSetsNotSupported
elif isinstance(args[0], (list, tuple)):
for partition in args[0]:
self.append(partition)
self.wrap_nested()
def wrap_nested(self) -> None:
"""Ensure that all nested data structures are wrapped as PyVista datasets.
This is performed in place.
"""
for i in range(self.n_partitions):
partition = self.GetPartition(i)
if not is_pyvista_dataset(partition):
self.SetPartition(i, wrap(partition))
@overload
def __getitem__(self, index: int) -> DataSet | None: ... # pragma: no cover
@overload
def __getitem__(self, index: slice) -> PartitionedDataSet: ... # pragma: no cover
def __getitem__(self, index):
"""Get a partition by its index."""
if isinstance(index, slice):
return PartitionedDataSet([self[i] for i in range(self.n_partitions)[index]])
else:
if index < -self.n_partitions or index >= self.n_partitions:
msg = f'index ({index}) out of range for this dataset.'
raise IndexError(msg)
if index < 0:
index = self.n_partitions + index
return wrap(self.GetPartition(index))
@overload
def __setitem__(self, index: int, data: DataSet | None) -> None: ... # pragma: no cover
@overload
def __setitem__(
self, index: slice, data: Iterable[DataSet | None]
) -> None: ... # pragma: no cover
def __setitem__(
self,
index: int | slice,
data,
):
"""Set a partition with a VTK data object."""
if isinstance(index, slice):
for i, d in zip(range(self.n_partitions)[index], data):
self.SetPartition(i, d)
else:
if index < -self.n_partitions or index >= self.n_partitions:
msg = f'index ({index}) out of range for this dataset.'
raise IndexError(msg)
if index < 0:
index = self.n_partitions + index
self.SetPartition(index, data)
def __delitem__(self, index: int | slice) -> None:
"""Remove a partition at the specified index are not supported."""
raise PartitionedDataSetsNotSupported
def insert(self, index: int, dataset: DataSet) -> None: # numpydoc ignore=PR01
"""Insert data before index."""
index = range(self.n_partitions)[index]
self.n_partitions += 1
for i in reversed(range(index, self.n_partitions - 1)):
self[i + 1] = self[i]
self[index] = dataset
def pop(self, index: int = -1) -> None: # numpydoc ignore=PR01 # noqa: ARG002
"""Pop off a partition at the specified index are not supported."""
raise PartitionedDataSetsNotSupported
def _get_attrs(self):
"""Return the representation methods (internal helper)."""
attrs = []
attrs.append(('N Partitions', self.n_partitions, '{}'))
return attrs
def _repr_html_(self) -> str:
"""Define a pretty representation for Jupyter notebooks."""
fmt = ''
fmt += "<table style='width: 100%;'>"
fmt += '<tr><th>Information</th><th>Partitions</th></tr>'
fmt += '<tr><td>'
fmt += '\n'
fmt += '<table>\n'
fmt += f'<tr><th>{type(self).__name__}</th><th>Values</th></tr>\n'
row = '<tr><td>{}</td><td>{}</td></tr>\n'
for attr in self._get_attrs():
try:
fmt += row.format(attr[0], attr[2].format(*attr[1]))
except TypeError:
fmt += row.format(attr[0], attr[2].format(attr[1]))
fmt += '</table>\n'
fmt += '\n'
fmt += '</td><td>'
fmt += '\n'
fmt += '<table>\n'
row = '<tr><th>{}</th><th>{}</th></tr>\n'
fmt += row.format('Index', 'Type')
for i in range(self.n_partitions):
data = self[i]
fmt += row.format(i, type(data).__name__)
fmt += '</table>\n'
fmt += '\n'
fmt += '</td></tr> </table>'
return fmt
def __repr__(self) -> str:
"""Define an adequate representation."""
fmt = f'{type(self).__name__} ({hex(id(self))})\n'
max_len = max(len(attr[0]) for attr in self._get_attrs()) + 4
row = f' {{:{max_len}s}}' + '{}\n'
for attr in self._get_attrs():
try:
fmt += row.format(attr[0], attr[2].format(*attr[1]))
except TypeError:
fmt += row.format(attr[0], attr[2].format(attr[1]))
return fmt.strip()
def __str__(self) -> str:
"""Return the str representation of the multi partition."""
return PartitionedDataSet.__repr__(self)
def __len__(self) -> int:
"""Return the number of partitions."""
return self.n_partitions
def copy_meta_from(self, ido, deep) -> None: # numpydoc ignore=PR01
"""Copy pyvista meta data onto this object from another object."""
@_deprecate_positional_args
def copy(self, deep: bool = True): # noqa: FBT001, FBT002
"""Return a copy of the PartitionedDataSet.
Parameters
----------
deep : bool, default: True
When ``True``, make a full copy of the object.
Returns
-------
pyvista.PartitionedDataSet
Deep or shallow copy of the ``PartitionedDataSet``.
Examples
--------
>>> import pyvista as pv
>>> data = [
... pv.Sphere(center=(2, 0, 0)),
... pv.Cube(center=(0, 2, 0)),
... pv.Cone(),
... ]
>>> partitions = pv.PartitionedDataSet(data)
>>> new_partitions = partitions.copy()
>>> len(new_partitions)
3
"""
thistype = type(self)
newobject = thistype()
if deep:
newobject.deep_copy(self)
else:
raise PartitionedDataSetsNotSupported
newobject.copy_meta_from(self, deep)
newobject.wrap_nested()
return newobject
@property
def n_partitions(self) -> int:
"""Return the number of partitions.
Returns
-------
int
The number of partitions.
"""
return self.GetNumberOfPartitions()
@n_partitions.setter
def n_partitions(self, n) -> None:
self.SetNumberOfPartitions(n)
self.Modified()
@property
def is_empty(self) -> bool: # numpydoc ignore=RT01
"""Return ``True`` if there are no partitions.
.. versionadded:: 0.46
Examples
--------
>>> import pyvista as pv
>>> mesh = pv.PartitionedDataSet()
>>> mesh.is_empty
True
>>> mesh.append(pv.Sphere())
>>> mesh.is_empty
False
"""
return self.n_partitions == 0
def append(self, dataset) -> None:
"""Add a data set to the next partition index.
Parameters
----------
dataset : pyvista.DataSet
Dataset to append to this partitioned dataset.
"""
index = self.n_partitions
self.n_partitions += 1
self[index] = dataset
def get_data_range( # numpydoc ignore=RT01
self: Self, name: str | None, preference: FieldAssociation | str
) -> tuple[float, float]: # pragma: no cover
"""Get the non-NaN min and max of a named array."""
return DataObject.get_data_range(self, name=name, preference=preference)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,135 @@
"""Contains pyvista_ndarray a numpy ndarray type used in pyvista."""
from __future__ import annotations
from collections.abc import Iterable
from typing import TYPE_CHECKING
from typing import cast
import numpy as np
from . import _vtk_core as _vtk
from .utilities.arrays import FieldAssociation
from .utilities.arrays import convert_array
from .utilities.misc import _NoNewAttrMixin
if TYPE_CHECKING:
from typing import Any
import numpy.typing as npt
from pyvista import DataSet
from ._typing_core import ArrayLike
from ._typing_core import NumpyArray
class pyvista_ndarray(_NoNewAttrMixin, np.ndarray): # numpydoc ignore=PR02 # noqa: N801
"""A ndarray which references the owning dataset and the underlying vtk array.
This array can be acted upon just like a :class:`numpy.ndarray`.
Parameters
----------
array : ArrayLike or :vtk:`vtkAbstractArray`
Array like.
dataset : DataSet
Input dataset.
association : pyvista.core.utilities.arrays.FieldAssociation
Field association.
Examples
--------
Return the points of a Sphere as a :class:`pyvista.pyvista_ndarray`.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mesh.points # doctest:+SKIP
pyvista_ndarray([[-5.5511151e-17, 0.0000000e+00, -5.0000000e-01],
[ 5.5511151e-17, 0.0000000e+00, 5.0000000e-01],
[-5.4059509e-02, 0.0000000e+00, -4.9706897e-01],
...,
[-1.5616201e-01, -3.3193260e-02, 4.7382659e-01],
[-1.0513641e-01, -2.2347433e-02, 4.8831028e-01],
[-5.2878179e-02, -1.1239604e-02, 4.9706897e-01]],
dtype=float32)
"""
def __new__( # noqa: PYI034
cls: type[pyvista_ndarray],
array: ArrayLike[float] | _vtk.vtkAbstractArray,
dataset: DataSet | _vtk.vtkDataSet | _vtk.VTKObjectWrapper | None = None,
association: FieldAssociation = FieldAssociation.NONE,
) -> pyvista_ndarray:
"""Allocate the array."""
if isinstance(array, _vtk.vtkAbstractArray):
obj = convert_array(array).view(cls)
obj.VTKObject = array
elif isinstance(array, Iterable):
obj = np.asarray(array).view(cls)
else:
msg = ( # type: ignore[unreachable]
f'pyvista_ndarray got an invalid type {type(array)}. '
'Expected an Iterable or vtk.vtkAbstractArray'
)
raise TypeError(msg)
obj.association = association
obj.dataset = _vtk.vtkWeakReference()
if isinstance(dataset, _vtk.VTKObjectWrapper):
obj.dataset.Set(dataset.VTKObject)
else:
obj.dataset.Set(cast('_vtk.vtkDataSet', dataset))
return obj
def __array_finalize__(self: pyvista_ndarray, obj: npt.NDArray[Any] | None) -> None:
"""Finalize array (associate with parent metadata)."""
# this is necessary to ensure that views/slices of pyvista_ndarray
# objects stay associated with those of their parents.
#
# the VTKArray class uses attributes called `DataSet` and `Association`
# to hold this data. I don't know why this class doesn't use the same
# convention, but here we just map those over to the appropriate
# attributes of this class
_vtk.VTKArray.__array_finalize__(self, obj) # type: ignore[arg-type]
if np.shares_memory(self, obj):
self.dataset = getattr(obj, 'dataset', None)
self.association = getattr(obj, 'association', FieldAssociation.NONE)
self.VTKObject = getattr(obj, 'VTKObject', None)
else:
self.dataset = None
self.association = FieldAssociation.NONE
self.VTKObject = None
def __setitem__(self: pyvista_ndarray, key: int | NumpyArray[int], value: Any) -> None: # type: ignore[override]
"""Implement [] set operator.
When the array is changed it triggers "Modified()" which updates
all upstream objects, including any render windows holding the
object.
"""
super().__setitem__(key, value)
if self.VTKObject is not None:
self.VTKObject.Modified()
# the associated dataset should also be marked as modified
dataset = self.dataset
if dataset is not None and dataset.Get():
dataset.Get().Modified()
def __array_wrap__(self: pyvista_ndarray, out_arr, context=None, return_scalar: bool = False): # noqa: ANN001, ANN204, FBT001, FBT002
"""Return a numpy scalar if array is 0d.
See https://github.com/numpy/numpy/issues/5819
"""
if out_arr.ndim:
return super().__array_wrap__(out_arr, context, return_scalar)
# Match numpy's behavior and return a numpy dtype scalar
return out_arr[()]
__getattr__ = _vtk.VTKObjectWrapperCheckSnakeCase.__getattr__
@@ -0,0 +1,235 @@
"""Utilities routines."""
from __future__ import annotations
import contextlib
from .arrays import FieldAssociation as FieldAssociation
from .arrays import array_from_vtkmatrix as array_from_vtkmatrix
from .arrays import cell_array as cell_array
from .arrays import convert_array as convert_array
from .arrays import convert_string_array as convert_string_array
from .arrays import field_array as field_array
from .arrays import get_array as get_array
from .arrays import get_array_association as get_array_association
from .arrays import get_vtk_type as get_vtk_type
from .arrays import parse_field_choice as parse_field_choice
from .arrays import point_array as point_array
from .arrays import raise_has_duplicates as raise_has_duplicates
from .arrays import raise_not_matching as raise_not_matching
from .arrays import row_array as row_array
from .arrays import set_default_active_scalars as set_default_active_scalars
from .arrays import set_default_active_vectors as set_default_active_vectors
from .arrays import vtk_bit_array_to_char as vtk_bit_array_to_char
from .arrays import vtk_id_list_to_array as vtk_id_list_to_array
from .arrays import vtkmatrix_from_array as vtkmatrix_from_array
from .cells import create_mixed_cells as create_mixed_cells
from .cells import get_mixed_cells as get_mixed_cells
from .cells import ncells_from_cells as ncells_from_cells
from .cells import numpy_to_idarr as numpy_to_idarr
from .features import cartesian_to_spherical as cartesian_to_spherical
from .features import create_grid as create_grid
from .features import grid_from_sph_coords as grid_from_sph_coords
from .features import merge as merge
from .features import perlin_noise as perlin_noise
from .features import sample_function as sample_function
from .features import spherical_to_cartesian as spherical_to_cartesian
from .features import transform_vectors_sph_to_cart as transform_vectors_sph_to_cart
from .features import voxelize as voxelize
from .features import voxelize_volume as voxelize_volume
from .fileio import from_meshio as from_meshio
from .fileio import get_ext as get_ext
from .fileio import is_meshio_mesh as is_meshio_mesh
from .fileio import read as read
from .fileio import read_exodus as read_exodus
from .fileio import read_grdecl as read_grdecl
from .fileio import read_meshio as read_meshio
from .fileio import read_pickle as read_pickle
from .fileio import read_texture as read_texture
from .fileio import save_meshio as save_meshio
from .fileio import save_pickle as save_pickle
from .fileio import set_pickle_format as set_pickle_format
from .fileio import set_vtkwriter_mode as set_vtkwriter_mode
from .fileio import to_meshio as to_meshio
from .geometric_objects import NORMALS as NORMALS
from .geometric_objects import Arrow as Arrow
from .geometric_objects import Box as Box
from .geometric_objects import Capsule as Capsule
from .geometric_objects import Circle as Circle
from .geometric_objects import CircularArc as CircularArc
from .geometric_objects import CircularArcFromNormal as CircularArcFromNormal
from .geometric_objects import Cone as Cone
from .geometric_objects import Cube as Cube
from .geometric_objects import Cylinder as Cylinder
from .geometric_objects import CylinderStructured as CylinderStructured
from .geometric_objects import Disc as Disc
from .geometric_objects import Dodecahedron as Dodecahedron
from .geometric_objects import Ellipse as Ellipse
from .geometric_objects import Icosahedron as Icosahedron
from .geometric_objects import Icosphere as Icosphere
from .geometric_objects import Line as Line
from .geometric_objects import MultipleLines as MultipleLines
from .geometric_objects import Octahedron as Octahedron
from .geometric_objects import Plane as Plane
from .geometric_objects import PlatonicSolid as PlatonicSolid
from .geometric_objects import Polygon as Polygon
from .geometric_objects import Pyramid as Pyramid
from .geometric_objects import Quadrilateral as Quadrilateral
from .geometric_objects import Rectangle as Rectangle
from .geometric_objects import SolidSphere as SolidSphere
from .geometric_objects import SolidSphereGeneric as SolidSphereGeneric
from .geometric_objects import Sphere as Sphere
from .geometric_objects import Superquadric as Superquadric
from .geometric_objects import Tetrahedron as Tetrahedron
from .geometric_objects import Text3D as Text3D
from .geometric_objects import Triangle as Triangle
from .geometric_objects import Tube as Tube
from .geometric_objects import Wavelet as Wavelet
from .geometric_sources import ArrowSource as ArrowSource
from .geometric_sources import AxesGeometrySource as AxesGeometrySource
from .geometric_sources import BoxSource as BoxSource
from .geometric_sources import ConeSource as ConeSource
from .geometric_sources import CubeFacesSource as CubeFacesSource
from .geometric_sources import CubeSource as CubeSource
from .geometric_sources import CylinderSource as CylinderSource
from .geometric_sources import DiscSource as DiscSource
from .geometric_sources import LineSource as LineSource
from .geometric_sources import MultipleLinesSource as MultipleLinesSource
from .geometric_sources import OrthogonalPlanesSource as OrthogonalPlanesSource
from .geometric_sources import PlaneSource as PlaneSource
from .geometric_sources import PlatonicSolidSource as PlatonicSolidSource
from .geometric_sources import PolygonSource as PolygonSource
from .geometric_sources import SphereSource as SphereSource
from .geometric_sources import SuperquadricSource as SuperquadricSource
from .geometric_sources import Text3DSource as Text3DSource
from .geometric_sources import translate as translate
from .image_sources import ImageEllipsoidSource as ImageEllipsoidSource
from .image_sources import ImageGaussianSource as ImageGaussianSource
from .image_sources import ImageGridSource as ImageGridSource
from .image_sources import ImageMandelbrotSource as ImageMandelbrotSource
from .image_sources import ImageNoiseSource as ImageNoiseSource
from .image_sources import ImageSinusoidSource as ImageSinusoidSource
with contextlib.suppress(ImportError):
from .geometric_sources import CapsuleSource as CapsuleSource
from .cell_quality import cell_quality_info as cell_quality_info
from .helpers import axis_rotation as axis_rotation
from .helpers import generate_plane as generate_plane
from .helpers import is_inside_bounds as is_inside_bounds
from .helpers import is_pyvista_dataset as is_pyvista_dataset
from .helpers import wrap as wrap
from .misc import AnnotatedIntEnum as AnnotatedIntEnum
from .misc import abstract_class as abstract_class
from .misc import assert_empty_kwargs as assert_empty_kwargs
from .misc import check_valid_vector as check_valid_vector
from .misc import conditional_decorator as conditional_decorator
from .misc import has_module as has_module
from .misc import set_new_attribute as set_new_attribute
from .misc import threaded as threaded
from .misc import try_callback as try_callback
from .observers import Observer as Observer
from .observers import ProgressMonitor as ProgressMonitor
from .observers import VtkErrorCatcher as VtkErrorCatcher
from .observers import send_errors_to_logging as send_errors_to_logging
from .observers import set_error_output_file as set_error_output_file
from .parametric_objects import KochanekSpline as KochanekSpline
from .parametric_objects import ParametricBohemianDome as ParametricBohemianDome
from .parametric_objects import ParametricBour as ParametricBour
from .parametric_objects import ParametricBoy as ParametricBoy
from .parametric_objects import ParametricCatalanMinimal as ParametricCatalanMinimal
from .parametric_objects import ParametricConicSpiral as ParametricConicSpiral
from .parametric_objects import ParametricCrossCap as ParametricCrossCap
from .parametric_objects import ParametricDini as ParametricDini
from .parametric_objects import ParametricEllipsoid as ParametricEllipsoid
from .parametric_objects import ParametricEnneper as ParametricEnneper
from .parametric_objects import ParametricFigure8Klein as ParametricFigure8Klein
from .parametric_objects import ParametricHenneberg as ParametricHenneberg
from .parametric_objects import ParametricKlein as ParametricKlein
from .parametric_objects import ParametricKuen as ParametricKuen
from .parametric_objects import ParametricMobius as ParametricMobius
from .parametric_objects import ParametricPluckerConoid as ParametricPluckerConoid
from .parametric_objects import ParametricPseudosphere as ParametricPseudosphere
from .parametric_objects import ParametricRandomHills as ParametricRandomHills
from .parametric_objects import ParametricRoman as ParametricRoman
from .parametric_objects import ParametricSuperEllipsoid as ParametricSuperEllipsoid
from .parametric_objects import ParametricSuperToroid as ParametricSuperToroid
from .parametric_objects import ParametricTorus as ParametricTorus
from .parametric_objects import Spline as Spline
from .parametric_objects import parametric_keywords as parametric_keywords
from .parametric_objects import surface_from_para as surface_from_para
from .points import fit_line_to_points as fit_line_to_points
from .points import fit_plane_to_points as fit_plane_to_points
from .points import line_segments_from_points as line_segments_from_points
from .points import lines_from_points as lines_from_points
from .points import make_tri_mesh as make_tri_mesh
from .points import principal_axes as principal_axes
from .points import vector_poly_data as vector_poly_data
from .points import vtk_points as vtk_points
from .reader import AVSucdReader as AVSucdReader
from .reader import BaseReader as BaseReader
from .reader import BinaryMarchingCubesReader as BinaryMarchingCubesReader
from .reader import BMPReader as BMPReader
from .reader import BYUReader as BYUReader
from .reader import CGNSReader as CGNSReader
from .reader import DEMReader as DEMReader
from .reader import DICOMReader as DICOMReader
from .reader import EnSightReader as EnSightReader
from .reader import ExodusIIBlockSet as ExodusIIBlockSet
from .reader import ExodusIIReader as ExodusIIReader
from .reader import FacetReader as FacetReader
from .reader import FLUENTCFFReader as FLUENTCFFReader
from .reader import FluentReader as FluentReader
from .reader import GambitReader as GambitReader
from .reader import GaussianCubeReader as GaussianCubeReader
from .reader import GESignaReader as GESignaReader
from .reader import GIFReader as GIFReader
from .reader import GLTFReader as GLTFReader
from .reader import HDFReader as HDFReader
from .reader import HDRReader as HDRReader
from .reader import JPEGReader as JPEGReader
from .reader import MetaImageReader as MetaImageReader
from .reader import MFIXReader as MFIXReader
from .reader import MINCImageReader as MINCImageReader
from .reader import MultiBlockPlot3DReader as MultiBlockPlot3DReader
from .reader import Nek5000Reader as Nek5000Reader
from .reader import NIFTIReader as NIFTIReader
from .reader import NRRDReader as NRRDReader
from .reader import OBJReader as OBJReader
from .reader import OpenFOAMReader as OpenFOAMReader
from .reader import ParticleReader as ParticleReader
from .reader import PDBReader as PDBReader
from .reader import Plot3DFunctionEnum as Plot3DFunctionEnum
from .reader import Plot3DMetaReader as Plot3DMetaReader
from .reader import PLYReader as PLYReader
from .reader import PNGReader as PNGReader
from .reader import PNMReader as PNMReader
from .reader import PointCellDataSelection as PointCellDataSelection
from .reader import POpenFOAMReader as POpenFOAMReader
from .reader import ProStarReader as ProStarReader
from .reader import PTSReader as PTSReader
from .reader import PVDDataSet as PVDDataSet
from .reader import PVDReader as PVDReader
from .reader import SegYReader as SegYReader
from .reader import SLCReader as SLCReader
from .reader import STLReader as STLReader
from .reader import TecplotReader as TecplotReader
from .reader import TIFFReader as TIFFReader
from .reader import TimeReader as TimeReader
from .reader import VTKDataSetReader as VTKDataSetReader
from .reader import VTKPDataSetReader as VTKPDataSetReader
from .reader import XdmfReader as XdmfReader
from .reader import XMLImageDataReader as XMLImageDataReader
from .reader import XMLMultiBlockDataReader as XMLMultiBlockDataReader
from .reader import XMLPartitionedDataSetReader as XMLPartitionedDataSetReader
from .reader import XMLPImageDataReader as XMLPImageDataReader
from .reader import XMLPolyDataReader as XMLPolyDataReader
from .reader import XMLPRectilinearGridReader as XMLPRectilinearGridReader
from .reader import XMLPUnstructuredGridReader as XMLPUnstructuredGridReader
from .reader import XMLRectilinearGridReader as XMLRectilinearGridReader
from .reader import XMLStructuredGridReader as XMLStructuredGridReader
from .reader import XMLUnstructuredGridReader as XMLUnstructuredGridReader
from .reader import get_reader as get_reader
from .state_manager import vtk_snake_case as vtk_snake_case
from .state_manager import vtk_verbosity as vtk_verbosity
from .transform import Transform as Transform
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,334 @@
"""Information about cell quality measures."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import Literal
from typing import NoReturn
import numpy as np
from pyvista.core.celltype import _CELL_TYPE_INFO
from pyvista.core.celltype import CellType
from pyvista.core.utilities.misc import _NoNewAttrMixin
if TYPE_CHECKING:
from collections.abc import Sequence
_CellQualityLiteral = Literal[
'area',
'aspect_frobenius',
'aspect_gamma',
'aspect_ratio',
'collapse_ratio',
'condition',
'diagonal',
'dimension',
'distortion',
'jacobian',
'max_angle',
'max_aspect_frobenius',
'max_edge_ratio',
'med_aspect_frobenius',
'min_angle',
'oddy',
'radius_ratio',
'relative_size_squared',
'scaled_jacobian',
'shape',
'shape_and_size',
'shear',
'shear_and_size',
'skew',
'stretch',
'taper',
'volume',
'warpage',
]
_CellTypesLiteral = Literal[
CellType.TRIANGLE,
CellType.QUAD,
CellType.TETRA,
CellType.HEXAHEDRON,
CellType.PYRAMID,
CellType.WEDGE,
]
_CellTypeNamesLiteral = Literal[
'TRIANGLE',
'triangle',
'QUAD',
'quad',
'TETRA',
'tetra',
'HEXAHEDRON',
'hexahedron',
'PYRAMID',
'pyramid',
'WEDGE',
'wedge',
]
@dataclass
class CellQualityInfo(_NoNewAttrMixin):
"""Information about a cell's quality measure."""
cell_type: _CellTypesLiteral
quality_measure: _CellQualityLiteral
acceptable_range: tuple[float, float]
normal_range: tuple[float, float]
full_range: tuple[float, float]
unit_cell_value: float
def sqrt(num: float) -> float: # noqa: D103
return num**0.5
# Define aliases to help definitions fit on one line
INF = float('inf')
ANGLE = float((180 / np.pi) * np.arccos(1 / 3))
R22 = sqrt(2) / 2
R33 = sqrt(3) / 3
TRIANGLE: Literal[CellType.TRIANGLE] = CellType.TRIANGLE
QUAD: Literal[CellType.QUAD] = CellType.QUAD
TETRA: Literal[CellType.TETRA] = CellType.TETRA
HEXAHEDRON: Literal[CellType.HEXAHEDRON] = CellType.HEXAHEDRON
PYRAMID: Literal[CellType.PYRAMID] = CellType.PYRAMID
WEDGE: Literal[CellType.WEDGE] = CellType.WEDGE
Info = CellQualityInfo
_CELL_QUALITY_INFO = [
Info(TRIANGLE, 'area', (0.0, INF), (0.0, INF), (0.0, INF), sqrt(3.0) / 4.0),
Info(TRIANGLE, 'aspect_ratio', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(TRIANGLE, 'aspect_frobenius', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(TRIANGLE, 'condition', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(TRIANGLE, 'distortion', (0.5, 1.0), (0.0, 1.0), (-INF, INF), 1.0),
Info(TRIANGLE, 'max_angle', (60.0, 90.0), (60.0, 180.0), (0.0, 180.0), 60.0),
Info(TRIANGLE, 'min_angle', (30.0, 60.0), (0.0, 60.0), (0.0, 360.0), 60.0),
Info(TRIANGLE, 'scaled_jacobian', (0.5, 2 * R33), (-2 * R33, 2 * R33), (-INF, INF), 1.0),
Info(TRIANGLE, 'radius_ratio', (1.0, 3.0), (1.0, INF), (1.0, INF), 1.0),
Info(TRIANGLE, 'shape', (0.25, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(TRIANGLE, 'shape_and_size', (0.25, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(QUAD, 'area', (0.0, INF), (0.0, INF), (-INF, INF), 1.0),
Info(QUAD, 'aspect_ratio', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(QUAD, 'condition', (1.0, 4), (1.0, INF), (1.0, INF), 1.0),
Info(QUAD, 'distortion', (0.5, 1.0), (0.0, 1.0), (-INF, INF), 1.0),
Info(QUAD, 'jacobian', (0.0, INF), (0.0, INF), (-INF, INF), 1.0),
Info(QUAD, 'max_aspect_frobenius', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(QUAD, 'max_angle', (90.0, 135.0), (90.0, 360.0), (0.0, 360.0), 90.0),
Info(QUAD, 'max_edge_ratio', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(QUAD, 'med_aspect_frobenius', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(QUAD, 'min_angle', (45.0, 90.0), (0.0, 90.0), (0.0, 360.0), 90.0),
Info(QUAD, 'oddy', (0.0, 0.5), (0.0, INF), (0.0, INF), 0.0),
Info(QUAD, 'radius_ratio', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(QUAD, 'relative_size_squared', (0.3, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(QUAD, 'scaled_jacobian', (0.3, 1.0), (-1.0, 1.0), (-1.0, 1.0), 1.0),
Info(QUAD, 'shape', (0.3, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(QUAD, 'shape_and_size', (0.2, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(QUAD, 'shear', (0.3, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(QUAD, 'shear_and_size', (0.2, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(QUAD, 'skew', (0.0, 0.5), (0.0, 1.0), (0.0, 1.0), 0.0),
Info(QUAD, 'stretch', (0.25, 1.0), (0.0, 1.0), (0.0, INF), 1.0),
Info(QUAD, 'taper', (0.0, 0.7), (0.0, INF), (0.0, INF), 0.0),
Info(QUAD, 'warpage', (0.3, 1.0), (-1.0, 1.0), (-INF, INF), 1.0),
Info(TETRA, 'aspect_frobenius', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(TETRA, 'aspect_gamma', (1.0, 3.0), (1.0, INF), (1.0, INF), 1.0),
Info(TETRA, 'aspect_ratio', (1.0, 3.0), (1.0, INF), (1.0, INF), 1.0),
Info(TETRA, 'collapse_ratio', (0.1, INF), (0.0, INF), (0.0, INF), sqrt(6.0) / 3.0),
Info(TETRA, 'condition', (1.0, 3), (1.0, INF), (1.0, INF), 1.0),
Info(TETRA, 'distortion', (0.5, 1.0), (0.0, 1.0), (-INF, INF), 1.0),
Info(TETRA, 'jacobian', (0.0, INF), (0.0, INF), (-INF, INF), R22),
Info(TETRA, 'min_angle', (40, ANGLE), (0.0, ANGLE), (0.0, 360), ANGLE),
Info(TETRA, 'radius_ratio', (1.0, 3), (1.0, INF), (1.0, INF), 1.0),
Info(TETRA, 'relative_size_squared', (0.3, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(TETRA, 'scaled_jacobian', (0.5, 1.0), (-1.0, 1.0), (-INF, INF), 1.0),
Info(TETRA, 'shape', (0.3, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(TETRA, 'shape_and_size', (0.2, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(TETRA, 'volume', (0.0, INF), (-INF, INF), (-INF, INF), sqrt(2.0) / 12.0),
Info(HEXAHEDRON, 'diagonal', (0.65, 1.0), (0.0, 1.0), (0.0, INF), 1.0),
Info(HEXAHEDRON, 'dimension', (0.0, INF), (0.0, INF), (0.0, INF), R33),
Info(HEXAHEDRON, 'distortion', (0.5, 1.0), (0.0, 1.0), (-INF, INF), 1.0),
Info(HEXAHEDRON, 'jacobian', (0.0, INF), (0.0, INF), (-INF, INF), 1.0),
Info(HEXAHEDRON, 'max_edge_ratio', (1.0, 1.3), (1.0, INF), (1.0, INF), 1.0),
Info(HEXAHEDRON, 'max_aspect_frobenius', (1.0, 3), (1.0, INF), (1.0, INF), 1.0),
Info(HEXAHEDRON, 'med_aspect_frobenius', (1.0, 3), (1.0, INF), (1.0, INF), 1.0),
Info(HEXAHEDRON, 'oddy', (0.0, 0.5), (0.0, INF), (0.0, INF), 0.0),
Info(HEXAHEDRON, 'relative_size_squared', (0.5, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(HEXAHEDRON, 'scaled_jacobian', (0.5, 1.0), (-1.0, 1.0), (-1.0, INF), 1.0),
Info(HEXAHEDRON, 'shape', (0.3, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(HEXAHEDRON, 'shape_and_size', (0.2, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(HEXAHEDRON, 'shear', (0.3, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(HEXAHEDRON, 'shear_and_size', (0.2, 1.0), (0.0, 1.0), (0.0, 1.0), 1.0),
Info(HEXAHEDRON, 'skew', (0.0, 0.5), (0.0, 1.0), (0.0, INF), 0.0),
Info(HEXAHEDRON, 'stretch', (0.25, 1.0), (0.0, 1.0), (0.0, INF), 1.0),
Info(HEXAHEDRON, 'taper', (0.0, 0.5), (0.0, INF), (0.0, INF), 0.0),
Info(HEXAHEDRON, 'volume', (0.0, INF), (0.0, INF), (-INF, INF), 1.0),
Info(PYRAMID, 'volume', (0.0, INF), (-INF, INF), (-INF, INF), sqrt(2.0) / 6.0),
Info(WEDGE, 'volume', (0.0, INF), (-INF, INF), (-INF, INF), sqrt(3.0) / 4.0),
]
# Create lookup dict
_CELL_QUALITY_LOOKUP: dict[CellType, dict[_CellQualityLiteral, CellQualityInfo]] = {}
for info in _CELL_QUALITY_INFO:
_CELL_QUALITY_LOOKUP.setdefault(info.cell_type, {})
_CELL_QUALITY_LOOKUP[info.cell_type][info.quality_measure] = info
_CELL_TYPE_NAMES = [typ.name for typ in _CELL_QUALITY_LOOKUP.keys()]
def cell_quality_info(
cell_type: _CellTypesLiteral | _CellTypeNamesLiteral,
quality_measure: _CellQualityLiteral,
) -> CellQualityInfo:
"""Return information about a cell's quality measure.
This function returns information about a quality measure computed by
:meth:`~pyvista.DataObjectFilters.cell_quality` for a specified
:class:`~pyvista.CellType`. The following is provided for each measure:
- ``acceptable_range``: Well-behaved cells have values in this range.
- ``normal_range``: All cells except those with degeneracies have values in this range.
- ``full_range``: All cells including degenerate ones have values in this range.
- ``unit_cell_value``: The quality measure value for a reference unit cell (e.g.
equilateral triangle with edge length of one for triangles).
This information can help inform if a particular cell is of high or low quality.
See the tables below for a summary of all cell quality info available from this
function.
.. include:: /api/core/cell_quality/cell_quality_info_table_TRIANGLE.rst
.. include:: /api/core/cell_quality/cell_quality_info_table_QUAD.rst
.. include:: /api/core/cell_quality/cell_quality_info_table_HEXAHEDRON.rst
.. include:: /api/core/cell_quality/cell_quality_info_table_TETRA.rst
.. include:: /api/core/cell_quality/cell_quality_info_table_WEDGE.rst
.. include:: /api/core/cell_quality/cell_quality_info_table_PYRAMID.rst
.. note::
The information returned by this function is based on the
`Verdict Library Reference Manual <https://github.com/sandialabs/verdict/raw/master/SAND2007-2853p.pdf>`_.
.. note::
Information is not available for all valid quality measures computed by
:meth:`~pyvista.DataObjectFilters.cell_quality`. Only a subset
is provided here. If information about a measure is missing and you have
knowledge about its acceptable range, normal range, etc., please consider
submitting a pull request on GitHub at https://github.com/pyvista/pyvista.
Parameters
----------
cell_type : CellType | str
Cell type to get information about. May be a :class:`~pyvista.CellType` or the
name of a cell type as a string.
quality_measure : str
Quality measure to get information about. May be any quality measure from
:ref:`cell_quality_measures_table`.
Returns
-------
CellQualityInfo
Dataclass with information about the quality measure for a specific cell type.
Raises
------
ValueError
If info is not available for the specified cell type or measure.
See Also
--------
:meth:`~pyvista.DataObjectFilters.cell_quality`
Examples
--------
Get cell quality info for :attr:`~pyvista.CellType.TRIANGLE` cells and the
``'scaled_jacobian'`` quality measure.
>>> import pyvista as pv
>>> info_tri = pv.cell_quality_info(pv.CellType.TRIANGLE, 'scaled_jacobian')
>>> info_tri # doctest: +NORMALIZE_WHITESPACE
CellQualityInfo(cell_type=<CellType.TRIANGLE: 5>,
quality_measure='scaled_jacobian',
acceptable_range=(0.5, 1.1547005383792515),
normal_range=(-1.1547005383792515, 1.1547005383792515),
full_range=(-inf, inf),
unit_cell_value=1.0)
Show the acceptable range for this measure.
>>> info_tri.acceptable_range
(0.5, 1.1547005383792515)
Show the value of this measure for equilateral triangles with edge length of one.
>>> info_tri.unit_cell_value
1.0
Get info for the same measure but for :attr:`~pyvista.CellType.QUAD` cells.
>>> info_quad = pv.cell_quality_info(pv.CellType.QUAD, 'scaled_jacobian')
>>> info_quad # doctest: +NORMALIZE_WHITESPACE
CellQualityInfo(cell_type=<CellType.QUAD: 9>,
quality_measure='scaled_jacobian',
acceptable_range=(0.3, 1.0),
normal_range=(-1.0, 1.0),
full_range=(-1.0, 1.0),
unit_cell_value=1.0)
Show the acceptable range. Note that it differs for quads compared to triangles.
>>> info_quad.acceptable_range
(0.3, 1.0)
Show the value of this measure for a square cell with edge length of one.
>>> info_quad.unit_cell_value
1.0
See :ref:`mesh_quality_example` for more examples using this function.
"""
def raise_error(item_: str, valid_options_: Sequence[str]) -> NoReturn:
msg = (
f'Cell quality info is not available for {item_}. Valid options are:\n{valid_options_}'
)
raise ValueError(msg)
if isinstance(cell_type, str):
upper = cell_type.upper()
if upper not in _CELL_TYPE_NAMES:
item = f'cell type {upper!r}'
raise_error(item, _CELL_TYPE_NAMES)
value = CellType(_CELL_TYPE_INFO[upper].value)
else:
value = CellType(cell_type)
# Lookup measures available for the cell type
try:
measures = _CELL_QUALITY_LOOKUP[value]
except KeyError:
item = f'cell type {value.name!r}'
raise_error(item, _CELL_TYPE_NAMES)
# Lookup the measure info
try:
return measures[quality_measure]
except KeyError:
item = f'{value.name!r} measure {quality_measure!r}'
valid_options = list(measures.keys())
raise_error(item, valid_options)
@@ -0,0 +1,312 @@
"""PyVista wrapping of :vtk:`vtkCellArray`."""
from __future__ import annotations
from collections import deque
from itertools import count
from itertools import islice
from typing import TYPE_CHECKING
from typing import Literal
from typing import overload
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.celltype import _CELL_TYPE_TO_NUM_POINTS
if TYPE_CHECKING:
from pyvista import UnstructuredGrid
from pyvista.core._typing_core import ArrayLike
from pyvista.core._typing_core import NumpyArray
def ncells_from_cells(cells: NumpyArray[int]) -> int:
"""Get the number of cells from a VTK cell connectivity array.
Parameters
----------
cells : numpy.ndarray
A VTK cell connectivity array.
Returns
-------
int
The number of cells extracted from the given cell connectivity array.
"""
consumer: deque[NumpyArray[int]] = deque(maxlen=0)
it = cells.flat
for n_cells in count(): # noqa: B007
skip = next(it, None)
if skip is None:
break
consumer.extend(islice(it, skip)) # type: ignore[arg-type]
return n_cells
@overload
def numpy_to_idarr(
ind: int | ArrayLike[int],
deep: bool = ..., # noqa: FBT001
return_ind: Literal[True] = True, # noqa: FBT002
) -> _vtk.vtkIdTypeArray: ...
@overload
def numpy_to_idarr(
ind: int | ArrayLike[int],
deep: bool = ..., # noqa: FBT001
return_ind: Literal[False] = False, # noqa: FBT002
) -> tuple[_vtk.vtkIdTypeArray, NumpyArray[int]]: ...
@overload
def numpy_to_idarr(
ind: int | ArrayLike[int],
deep: bool = ..., # noqa: FBT001
return_ind: bool = ..., # noqa: FBT001
) -> tuple[_vtk.vtkIdTypeArray, NumpyArray[int]] | _vtk.vtkIdTypeArray: ...
@_deprecate_positional_args(allowed=['ind'])
def numpy_to_idarr(
ind: int | ArrayLike[int],
deep: bool = False, # noqa: FBT001, FBT002
return_ind: bool = False, # noqa: FBT001, FBT002
) -> tuple[_vtk.vtkIdTypeArray, NumpyArray[int]] | _vtk.vtkIdTypeArray:
"""Safely convert a numpy array to a :vtk:`vtkIdTypeArray`.
Parameters
----------
ind : sequence[int]
Input sequence to be converted to a :vtk:`vtkIdTypeArray`. Can be either a mask
or an integer array-like.
deep : bool, default: False
If ``True``, deep copy the input data. If ``False``, do not deep copy
the input data.
return_ind : bool, default: False
If ``True``, also return the input array after it has been cast to the
proper dtype.
Returns
-------
:vtk:`vtkIdTypeArray`
Converted array as a :vtk:`vtkIdTypeArray`.
numpy.ndarray
The input array after it has been cast to the proper dtype. Only
returned if `return_ind` is set to ``True``.
Raises
------
TypeError
If the input array is not a mask or an integer array-like.
"""
ind = np.asarray(ind)
# np.asarray will eat anything, so we have to weed out bogus inputs
if not issubclass(ind.dtype.type, (np.bool_, np.integer)):
msg = 'Indices must be either a mask or an integer array-like'
raise TypeError(msg)
if ind.dtype == np.bool_:
ind = ind.nonzero()[0].astype(pyvista.ID_TYPE)
elif ind.dtype != pyvista.ID_TYPE:
ind = ind.astype(pyvista.ID_TYPE)
elif not ind.flags['C_CONTIGUOUS']:
ind = np.ascontiguousarray(ind, dtype=pyvista.ID_TYPE)
# must ravel or segfault when saving MultiBlock
vtk_idarr = _vtk.numpy_to_vtkIdTypeArray(ind.ravel(), deep=deep)
if return_ind:
return vtk_idarr, ind
return vtk_idarr
def create_mixed_cells(
mixed_cell_dict: dict[np.uint8, NumpyArray[int]], nr_points: int | None = None
) -> tuple[NumpyArray[np.uint8], NumpyArray[int]]:
"""Generate cell arrays for the creation of a pyvista.UnstructuredGrid from a cell dictionary.
This function generates all required cell arrays according to a given cell
dictionary. The given cell-dictionary should contain a proper
mapping of vtk_type -> np.ndarray (int), where the given ndarray
for each cell-type has to be an array of dimensions [N, D] or
[N*D], where N is the number of cells and D is the size of the
cells for the given type (e.g. 3 for triangles). Multiple
vtk_type keys with associated arrays can be present in one
dictionary. This function only accepts cell types of fixed size
and not dynamic sized cells like :attr:`~pyvista.CellType.POLYGON`
Parameters
----------
mixed_cell_dict : dict
A dictionary that maps VTK-Enum-types (e.g. :attr:`~pyvista.CellType.TRIANGLE`) to
np.ndarrays of type int. The ``np.ndarrays`` describe the cell
connectivity.
nr_points : int, optional
Number of points of the grid. Used only to allow additional runtime
checks for invalid indices.
Returns
-------
cell_types : numpy.ndarray (uint8)
Types of each cell.
cell_arr : numpy.ndarray (int)
VTK-cell array.
Raises
------
ValueError
If any of the cell types are not supported, have dynamic sized
cells, map to values with wrong size, or cell indices point
outside the given number of points.
Examples
--------
Create the cell arrays containing two triangles.
This will generate cell arrays to generate a mesh with two
disconnected triangles from 6 points.
>>> import numpy as np
>>> import vtk
>>> from pyvista.core.utilities.cells import create_mixed_cells
>>> cell_types, cell_arr = create_mixed_cells(
... {vtk.VTK_TRIANGLE: np.array([[0, 1, 2], [3, 4, 5]])}
... )
"""
if not np.all([k in _CELL_TYPE_TO_NUM_POINTS for k in mixed_cell_dict.keys()]):
msg = 'Found unknown or unsupported VTK cell type in your requested cells'
raise ValueError(msg)
if not np.all([_CELL_TYPE_TO_NUM_POINTS[k] > 0 for k in mixed_cell_dict.keys()]):
msg = "You requested a cell type with variable length, which can't be used in this method"
raise ValueError(msg)
final_cell_types = []
final_cell_arr = []
for elem_t, cells_arr in mixed_cell_dict.items():
nr_points_per_elem = _CELL_TYPE_TO_NUM_POINTS[elem_t]
if (
not isinstance(cells_arr, np.ndarray) # type: ignore[redundant-expr]
or not np.issubdtype(cells_arr.dtype, np.integer)
or cells_arr.ndim not in [1, 2]
or (cells_arr.ndim == 1 and cells_arr.size % nr_points_per_elem != 0)
or (cells_arr.ndim == 2 and cells_arr.shape[-1] != nr_points_per_elem)
):
msg = (
f'Expected an np.ndarray of size [N, {nr_points_per_elem}] or '
f'[N*{nr_points_per_elem}] with an integral type'
)
raise ValueError(msg)
if np.any(cells_arr < 0):
msg = f'Non-valid index (<0) given for cells of type {elem_t}'
raise ValueError(msg)
if nr_points is not None and np.any(cells_arr >= nr_points):
msg = f'Non-valid index (>={nr_points}) given for cells of type {elem_t}'
raise ValueError(msg)
# Ensure array is not flat
cells_arr_not_flat = (
cells_arr.reshape([-1, nr_points_per_elem]) if cells_arr.ndim == 1 else cells_arr
)
nr_elems = cells_arr_not_flat.shape[0]
final_cell_types.append(np.array([elem_t] * nr_elems, dtype=np.uint8))
final_cell_arr.append(
np.concatenate(
[
np.ones_like(cells_arr_not_flat[..., :1]) * nr_points_per_elem,
cells_arr_not_flat,
],
axis=-1,
).reshape([-1]),
)
cell_types_out = np.concatenate(final_cell_types)
cell_arr_out = np.concatenate(final_cell_arr)
return cell_types_out, cell_arr_out
def get_mixed_cells(vtkobj: UnstructuredGrid) -> dict[np.uint8, NumpyArray[int]]:
"""Create the cells dictionary from the given pyvista.UnstructuredGrid.
This functions creates a cells dictionary (see
create_mixed_cells), with a mapping vtk_type -> np.ndarray (int)
for fixed size cell types. The returned dictionary will have
arrays of size [N, D], where N is the number of cells and D is the
size of the cells for the given type (e.g. 3 for triangles).
.. versionchanged:: 0.46
An empty dict ``{}`` is returned instead of ``None`` if the input
is empty.
Parameters
----------
vtkobj : pyvista.UnstructuredGrid
The unstructured grid for which the cells dictionary should be computed.
Returns
-------
dict
Dictionary of cells.
Raises
------
ValueError
If vtkobj is not a pyvista.UnstructuredGrid, any of the
present cells are unsupported, or have dynamic cell sizes,
like VTK_POLYGON.
"""
return_dict: dict[np.uint8, NumpyArray[int]] = {}
if not isinstance(vtkobj, pyvista.UnstructuredGrid):
msg = 'Expected a pyvista object' # type: ignore[unreachable]
raise TypeError(msg)
nr_cells = vtkobj.n_cells
if nr_cells == 0:
return return_dict
cell_types = vtkobj.celltypes
cells = vtkobj.cells
unique_cell_types = np.unique(cell_types)
if not np.all([k in _CELL_TYPE_TO_NUM_POINTS for k in unique_cell_types]):
msg = 'Found unknown or unsupported VTK cell type in the present cells'
raise ValueError(msg)
if not np.all([_CELL_TYPE_TO_NUM_POINTS[k] > 0 for k in unique_cell_types]):
msg = (
'You requested a cell-dictionary with a variable length cell, which is not supported '
'currently'
)
raise ValueError(msg)
cell_sizes = np.zeros_like(cell_types)
for cell_type in unique_cell_types:
mask = cell_types == cell_type
cell_sizes[mask] = _CELL_TYPE_TO_NUM_POINTS[cell_type]
cell_ends = np.cumsum(cell_sizes + 1)
cell_starts = np.concatenate([np.array([0], dtype=cell_ends.dtype), cell_ends[:-1]]) + 1
for cell_type in unique_cell_types:
cell_size = _CELL_TYPE_TO_NUM_POINTS[cell_type]
mask = cell_types == cell_type
current_cell_starts = cell_starts[mask]
cells_inds = current_cell_starts[..., np.newaxis] + np.arange(cell_size)[
np.newaxis
].astype(
cell_starts.dtype,
)
return_dict[cell_type] = cells[cells_inds]
return return_dict
@@ -0,0 +1,153 @@
"""Supporting functions for documentation build."""
from __future__ import annotations
import inspect
import os
import os.path as op
import sys
def linkcode_resolve(domain: str, info: dict[str, str], edit: bool = False) -> str | None: # noqa: FBT001, FBT002
"""Determine the URL corresponding to a Python object.
Parameters
----------
domain : str
Only useful when 'py'.
info : dict
With keys "module" and "fullname".
edit : bool, default=False
Jump right to the edit page.
Returns
-------
str
The code URL. Empty string if there is no valid link.
Notes
-----
This function is used by the `sphinx.ext.linkcode` extension to create the "[Source]"
button whose link is edited in this function.
This has been adapted to deal with our "verbose" decorator.
Adapted from mne (mne/utils/docs.py), which was adapted from SciPy (doc/source/conf.py).
"""
import pyvista # noqa: PLC0415
if domain != 'py':
return None
modname = info['module']
fullname = info['fullname']
# Little clean up to avoid pyvista.pyvista
if fullname.startswith(modname):
fullname = fullname[len(modname) + 1 :]
submod = sys.modules.get(modname)
if submod is None:
return None
obj = submod
for part in fullname.split('.'):
try:
obj = getattr(obj, part)
except Exception: # noqa: BLE001
return None
# deal with our decorators properly
while hasattr(obj, 'fget'):
obj = obj.fget
# deal with wrapped object
while hasattr(obj, '__wrapped__'):
obj = obj.__wrapped__
try:
fn = inspect.getsourcefile(obj)
except Exception: # noqa: BLE001 # pragma: no cover
fn = None
if not fn: # pragma: no cover
try:
fn = inspect.getsourcefile(sys.modules[obj.__module__])
except Exception: # noqa: BLE001
return None
return None
fn = op.relpath(fn, start=op.dirname(pyvista.__file__)) # noqa: PTH120
fn = '/'.join(op.normpath(fn).split(os.sep)) # in case on Windows # noqa: PTH206
try:
source, lineno = inspect.getsourcelines(obj)
except Exception: # noqa: BLE001 # pragma: no cover
lineno = None
linespec = f'#L{lineno}-L{lineno + len(source) - 1}' if lineno and not edit else ''
if 'dev' in pyvista.__version__:
kind = 'main'
else: # pragma: no cover
kind = f'release/{".".join(pyvista.__version__.split(".")[:2])}'
blob_or_edit = 'edit' if edit else 'blob'
return f'http://github.com/pyvista/pyvista/{blob_or_edit}/{kind}/pyvista/{fn}{linespec}'
def pv_html_page_context( # noqa: PLR0917
app, # noqa: ARG001
pagename: str,
templatename: str, # noqa: ARG001
context,
doctree, # noqa: ARG001
) -> None: # pragma: no cover
"""Add a function for returning an "edit this page" link pointing to `main`.
This is specific to PyVista to ensure that the "edit this page" link always
goes to the right page, specifically for:
- Gallery examples
- Autosummary examples (using _autosummary)
"""
def fix_edit_link_button(link: str) -> str | None:
"""Transform "edit on github" links to the correct url.
This is specific to PyVista to ensure that the "edit this page" link
always goes to the right page, specifically for:
- Gallery examples
- Autosummary examples (using _autosummary)
Parameters
----------
link : str
The link to the github edit interface.
Returns
-------
str
The link to the tip of the main branch for the same file.
"""
if pagename.startswith('examples') and 'index' not in pagename:
# This is a gallery example.
# We can get away with directly using the pagename since "examples"
# in the pagename is the same as the "examples" directory in the
# repo
return f'http://github.com/pyvista/pyvista/edit/main/{pagename}.py'
elif '_autosummary' in pagename:
# This is an API example
fullname = pagename.split('_autosummary')[1][1:]
return linkcode_resolve('py', {'module': 'pyvista', 'fullname': fullname}, edit=True)
else:
return link
context['fix_edit_link_button'] = fix_edit_link_button
@@ -0,0 +1,940 @@
"""Module containing geometry helper functions."""
from __future__ import annotations
from collections.abc import Sequence
import os
import sys
import warnings
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.errors import PyVistaDeprecationWarning
from pyvista.core.utilities.helpers import wrap
def _padded_bins(mesh, density):
"""Construct bin edges for voxelization.
Parameters
----------
mesh : pyvista.DataSet
Mesh to voxelize.
density : array_like[float]
A list of densities along x,y,z directions.
Returns
-------
list[np.ndarray]
List of bin edges for each axis.
Notes
-----
Ensures limits of voxelization are padded to ensure the mesh is fully enclosed.
"""
bounds = np.array(mesh.bounds).reshape(3, 2)
bin_count = np.ceil(1e-10 + (bounds[:, 1] - bounds[:, 0]) / density)
pad = (bin_count * density - (bounds[:, 1] - bounds[:, 0])) / 2
return [
np.arange(bounds[i, 0] - pad[i], bounds[i, 1] + pad[i] + density[i] / 2, density[i])
for i in range(3)
]
@_deprecate_positional_args(allowed=['mesh'])
def voxelize( # noqa: PLR0917
mesh,
density=None,
check_surface: bool = True, # noqa: FBT001, FBT002
enclosed: bool = False, # noqa: FBT001, FBT002
fit_bounds: bool = False, # noqa: FBT001, FBT002
):
"""Voxelize mesh to UnstructuredGrid.
.. deprecated:: 0.46
This function is deprecated. Use :meth:`pyvista.DataSetFilters.voxelize` instead.
Parameters
----------
mesh : pyvista.DataSet
Mesh to voxelize.
density : float | array_like[float]
The uniform size of the voxels when single float passed.
A list of densities along x,y,z directions.
Defaults to 1/100th of the mesh length.
check_surface : bool, default: True
Specify whether to check the surface for closure. If on, then the
algorithm first checks to see if the surface is closed and
manifold. If the surface is not closed and manifold, a runtime
error is raised.
enclosed : bool, default: False
If True, the voxel bounds will be outside the mesh.
If False, the voxel bounds will be at or inside the mesh bounds.
fit_bounds : bool, default: False
If enabled, the end bound of the input mesh is used as the end bound of the
voxel grid and the density is updated to the closest compatible one. Otherwise,
the end bound is excluded. Has no effect if `enclosed` is enabled.
Returns
-------
pyvista.UnstructuredGrid
Voxelized unstructured grid of the original mesh.
Notes
-----
Prior to version 0.39.0, this method improperly handled the order of
structured coordinates.
See Also
--------
pyvista.DataSetFilters.voxelize_rectilinear
Similar function that returns a :class:`pyvista.RectilinearGrid` with cell data.
pyvista.DataSetFilters.voxelize_binary_mask
Similar function that returns a :class:`pyvista.ImageData` with point data.
Examples
--------
Create an equal density voxelized mesh.
>>> import pyvista as pv
>>> from pyvista import examples
>>> mesh = examples.download_bunny_coarse().clean() # doctest:+SKIP
>>> vox = pv.voxelize(mesh, density=0.01) # doctest:+SKIP
>>> vox.plot(show_edges=True) # doctest:+SKIP
Create a voxelized mesh using unequal density dimensions.
>>> vox = pv.voxelize(mesh, density=[0.01, 0.005, 0.002]) # doctest:+SKIP
>>> vox.plot(show_edges=True) # doctest:+SKIP
Create an equal density voxel volume without enclosing input mesh.
>>> vox = pv.voxelize(mesh, density=0.01) # doctest:+SKIP
>>> vox = vox.select_enclosed_points(mesh, tolerance=0.0) # doctest:+SKIP
>>> vox.plot(scalars='SelectedPoints', show_edges=True) # doctest:+SKIP
Create an equal density voxel volume enclosing input mesh.
>>> vox = pv.voxelize(mesh, density=0.01, enclosed=True) # doctest:+SKIP
>>> vox = vox.select_enclosed_points(mesh, tolerance=0.0) # doctest:+SKIP
>>> vox.plot(scalars='SelectedPoints', show_edges=True) # doctest:+SKIP
Create a voxelized mesh that does not fit the input mesh's bounds. Notice the
cropped rectangular box.
>>> mesh = pv.Cube(x_length=0.25) # doctest:+SKIP
>>> vox = pv.voxelize(mesh=mesh, density=0.2) # doctest:+SKIP
>>> pl = pv.Plotter() # doctest:+SKIP
>>> _ = pl.add_mesh(mesh=vox, show_edges=True, color='yellow') # doctest:+SKIP
>>> _ = pl.add_mesh(
... mesh=mesh, show_edges=True, line_width=5, opacity=0.4
... ) # doctest:+SKIP
>>> pl.show() # doctest:+SKIP
Create a voxelized mesh that fits the input mesh's bounds. The rectangular mesh is
now complete. Notice that the voxel size was updated to fit the bounds in the first
direction.
>>> vox = pv.voxelize(mesh=mesh, density=0.2, fit_bounds=True) # doctest:+SKIP
>>> pl = pv.Plotter() # doctest:+SKIP
>>> _ = pl.add_mesh(mesh=vox, show_edges=True, color='yellow') # doctest:+SKIP
>>> _ = pl.add_mesh(
... mesh=mesh, show_edges=True, line_width=5, opacity=0.4
... ) # doctest:+SKIP
>>> pl.show() # doctest:+SKIP
"""
# Deprecated on v0.46.0, estimated removal on v0.49.0
warnings.warn(
'`pyvista.voxelize` is deprecated. Use `pyvista.DataSetFilters.voxelize` instead.',
PyVistaDeprecationWarning,
)
return _voxelize_legacy(
mesh=mesh,
density=density,
check_surface=check_surface,
enclosed=enclosed,
fit_bounds=fit_bounds,
)
def _voxelize_legacy(
mesh,
*,
density=None,
check_surface: bool = True,
enclosed: bool = False,
fit_bounds: bool = False,
):
"""Voxelize mesh to UnstructuredGrid.
The public `voxelize` function is deprecated but we need to keep it for
generating the PyVista logo.
"""
if not pyvista.is_pyvista_dataset(mesh):
mesh = wrap(mesh)
if density is None:
density = mesh.length / 100
if isinstance(density, (int, float, np.number)):
density_x, density_y, density_z = [density] * 3
elif isinstance(density, (Sequence, np.ndarray)):
density_x, density_y, density_z = density
else:
msg = f'Invalid density {density!r}, expected number or array-like.'
raise TypeError(msg)
# check and pre-process input mesh
surface = mesh.extract_geometry() # filter preserves topology
if not surface.faces.size:
# we have a point cloud or an empty mesh
msg = 'Input mesh must have faces for voxelization.'
raise ValueError(msg)
if not surface.is_all_triangles:
# reduce chance for artifacts, see gh-1743
surface.triangulate(inplace=True)
if enclosed:
# Get x, y, z bin edges
x, y, z = _padded_bins(mesh, [density_x, density_y, density_z])
else:
x_min, x_max, y_min, y_max, z_min, z_max = mesh.bounds
if fit_bounds:
# Calculate an integer number of voxels, floor to ensure that the voxels
# don't exceed the input mesh
nof_voxels_x = int(np.round((x_max - x_min) / density_x))
nof_voxels_y = int(np.round((y_max - y_min) / density_y))
nof_voxels_z = int(np.round((z_max - z_min) / density_z))
# One additional point is required to ensure the proper number of voxels
x = np.linspace(x_min, x_max, nof_voxels_x + 1)
y = np.linspace(y_min, y_max, nof_voxels_y + 1)
z = np.linspace(z_min, z_max, nof_voxels_z + 1)
else:
x = np.arange(x_min, x_max, density_x)
y = np.arange(y_min, y_max, density_y)
z = np.arange(z_min, z_max, density_z)
x, y, z = np.meshgrid(x, y, z, indexing='ij')
# indexing='ij' is used here in order to make grid and ugrid with x-y-z ordering,
# not y-x-z ordering, see https://github.com/pyvista/pyvista/pull/4365
# Create unstructured grid from the structured grid
grid = pyvista.StructuredGrid(x, y, z)
ugrid = pyvista.UnstructuredGrid(grid)
if enclosed:
# Normalise cells to unit size
ugrid_norm = ugrid.copy()
surface_norm = surface.copy()
ugrid_norm.points /= np.array(density)
surface_norm.points /= np.array(density)
# Select cells if they're within one unit of the surface
ugrid_norm = ugrid_norm.compute_implicit_distance(surface_norm)
mask = ugrid_norm['implicit_distance'] < 1
del ugrid_norm, surface_norm
else:
# get part of the mesh within the mesh's bounding surface.
selection = ugrid.select_enclosed_points(
surface, tolerance=0.0, check_surface=check_surface
)
mask = selection.point_data['SelectedPoints'].view(np.bool_)
del selection
# extract cells from point indices
return ugrid.extract_points(mask)
@_deprecate_positional_args(allowed=['mesh'])
def voxelize_volume( # noqa: PLR0917
mesh,
density=None,
check_surface: bool = True, # noqa: FBT001, FBT002
enclosed: bool = False, # noqa: FBT001, FBT002
fit_bounds: bool = False, # noqa: FBT001, FBT002
):
"""Voxelize mesh to create a RectilinearGrid voxel volume.
Creates a voxel volume that encloses the input mesh and discretizes the cells
within the volume that intersect or are contained within the input mesh.
``InsideMesh``, an array in ``cell_data``, is ``1`` for cells inside and ``0`` outside.
.. deprecated:: 0.46
This function is deprecated. Use :meth:`pyvista.DataSetFilters.voxelize_rectilinear`
instead.
Parameters
----------
mesh : pyvista.DataSet
Mesh to voxelize.
density : float | array_like[float]
The uniform size of the voxels when single float passed.
Nonuniform voxel size if a list of values are passed along x,y,z directions.
Defaults to 1/100th of the mesh length.
check_surface : bool, default: True
Specify whether to check the surface for closure. If on, then the
algorithm first checks to see if the surface is closed and
manifold. If the surface is not closed and manifold, a runtime
error is raised.
enclosed : bool, default: False
If True, the voxel bounds will be outside the mesh.
If False, the voxel bounds will be at or inside the mesh bounds.
fit_bounds : bool, default: False
If enabled, the end bound of the input mesh is used as the end bound of the
voxel grid and the density is updated to the closest compatible one. Otherwise,
the end bound is excluded. Has no effect if `enclosed` is enabled.
Returns
-------
pyvista.RectilinearGrid
RectilinearGrid as voxelized volume with discretized cells.
See Also
--------
pyvista.DataSetFilters.voxelize
Similar function that returns a :class:`pyvista.UnstructuredGrid` of
:attr:`~pyvista.CellType.VOXEL` cells.
pyvista.DataSetFilters.voxelize_binary_mask
Similar function that returns a :class:`pyvista.ImageData` with point data.
pyvista.DataSetFilters.select_enclosed_points
Examples
--------
Create an equal density voxel volume from input mesh.
>>> import pyvista as pv
>>> import numpy as np
Load file from PyVista examples.
>>> from pyvista import examples
>>> mesh = examples.download_cow() # doctest:+SKIP
Create an equal density voxel volume and plot the result.
>>> vox = pv.voxelize_volume(mesh, density=0.15) # doctest:+SKIP
>>> cpos = [(15, 3, 15), (0, 0, 0), (0, 0, 0)] # doctest:+SKIP
>>> vox.plot(scalars='InsideMesh', show_edges=True, cpos=cpos) # doctest:+SKIP
Slice the voxel volume to view ``InsideMesh``.
>>> slices = vox.slice_orthogonal() # doctest:+SKIP
>>> slices.plot(scalars='InsideMesh', show_edges=True) # doctest:+SKIP
Create a voxel volume from unequal density dimensions and plot result.
>>> vox = pv.voxelize_volume(mesh, density=[0.15, 0.15, 0.5]) # doctest:+SKIP
>>> vox.plot(scalars='InsideMesh', show_edges=True, cpos=cpos) # doctest:+SKIP
Slice the unequal density voxel volume to view ``InsideMesh``.
>>> slices = vox.slice_orthogonal() # doctest:+SKIP
>>> slices.plot(
... scalars='InsideMesh', show_edges=True, cpos=cpos
... ) # doctest:+SKIP
Create an equal density voxel volume without enclosing input mesh.
>>> vox = pv.voxelize_volume(mesh, density=0.15) # doctest:+SKIP
>>> vox = vox.select_enclosed_points(mesh, tolerance=0.0) # doctest:+SKIP
>>> vox.plot(
... scalars='SelectedPoints', show_edges=True, cpos=cpos
... ) # doctest:+SKIP
Create an equal density voxel volume enclosing input mesh.
>>> vox = pv.voxelize_volume(
... mesh, density=0.15, enclosed=True
... ) # doctest:+SKIP
>>> vox = vox.select_enclosed_points(mesh, tolerance=0.0) # doctest:+SKIP
>>> vox.plot(
... scalars='SelectedPoints', show_edges=True, cpos=cpos
... ) # doctest:+SKIP
Create an equal density voxel volume that does not fit the input mesh's bounds.
>>> mesh = pv.examples.load_nut() # doctest:+SKIP
>>> vox = pv.voxelize_volume(mesh=mesh, density=2.5) # doctest:+SKIP
>>> pl = pv.Plotter() # doctest:+SKIP
>>> _ = pl.add_mesh(mesh=vox, show_edges=True) # doctest:+SKIP
>>> _ = pl.add_mesh(mesh=mesh, show_edges=True, opacity=1) # doctest:+SKIP
>>> pl.show() # doctest:+SKIP
Create an equal density voxel volume that fits the input mesh's bounds.
>>> vox = pv.voxelize_volume(
... mesh=mesh, density=2.5, fit_bounds=True
... ) # doctest:+SKIP
>>> pl = pv.Plotter() # doctest:+SKIP
>>> _ = pl.add_mesh(mesh=vox, show_edges=True) # doctest:+SKIP
>>> _ = pl.add_mesh(mesh=mesh, show_edges=True, opacity=1) # doctest:+SKIP
>>> pl.show() # doctest:+SKIP
"""
# Deprecated on v0.46.0, estimated removal on v0.49.0
warnings.warn(
'`pyvista.voxelize_volume` is deprecated. Use '
'`pyvista.DataSetFilters.voxelize_rectilinear` instead.',
PyVistaDeprecationWarning,
)
mesh = wrap(mesh)
if density is None:
density = mesh.length / 100
if isinstance(density, (int, float, np.number)):
density_x, density_y, density_z = [density] * 3
elif isinstance(density, (Sequence, np.ndarray)):
density_x, density_y, density_z = density
else:
msg = f'Invalid density {density!r}, expected number or array-like.'
raise TypeError(msg)
# check and pre-process input mesh
surface = mesh.extract_geometry() # filter preserves topology
if not surface.faces.size:
# we have a point cloud or an empty mesh
msg = 'Input mesh must have faces for voxelization.'
raise ValueError(msg)
if not surface.is_all_triangles:
# reduce chance for artifacts, see gh-1743
surface.triangulate(inplace=True)
if enclosed:
# Get x, y, z bin edges
x, y, z = _padded_bins(mesh, [density_x, density_y, density_z])
else:
x_min, x_max, y_min, y_max, z_min, z_max = mesh.bounds
if fit_bounds:
# Calculate an integer number of voxels, floor to ensure that the voxels
# don't exceed the input mesh
nof_voxels_x = int(np.round((x_max - x_min) / density_x))
nof_voxels_y = int(np.round((y_max - y_min) / density_y))
nof_voxels_z = int(np.round((z_max - z_min) / density_z))
# One additional point is required to ensure the proper number of voxels
x = np.linspace(x_min, x_max, nof_voxels_x + 1)
y = np.linspace(y_min, y_max, nof_voxels_y + 1)
z = np.linspace(z_min, z_max, nof_voxels_z + 1)
else:
x = np.arange(x_min, x_max, density_x)
y = np.arange(y_min, y_max, density_y)
z = np.arange(z_min, z_max, density_z)
# Create a RectilinearGrid
voi = pyvista.RectilinearGrid(x, y, z)
# get part of the mesh within the mesh's bounding surface.
selection = voi.select_enclosed_points(surface, tolerance=0.0, check_surface=check_surface)
mask_vol = selection.point_data['SelectedPoints'].view(np.bool_)
# Get voxels that fall within input mesh boundaries
cell_ids = np.unique(voi.extract_points(np.argwhere(mask_vol))['vtkOriginalCellIds'])
# Create new element of grid where all cells _within_ mesh boundary are
# given new name 'MeshCells' and a discrete value of 1
voi['InsideMesh'] = np.zeros(voi.n_cells)
voi['InsideMesh'][cell_ids] = 1
return voi
def create_grid(dataset, dimensions=(101, 101, 101)):
"""Create a uniform grid surrounding the given dataset.
The output grid will have the specified dimensions and is commonly used
for interpolating the input dataset.
Parameters
----------
dataset : DataSet
Input dataset used as a reference for the grid creation.
dimensions : tuple[int, int, int], default: (101, 101, 101)
The dimensions of the grid to be created. Each value in the tuple
represents the number of grid points along the corresponding axis.
Raises
------
NotImplementedError
If the dimensions parameter is set to None. Currently, the function
does not support automatically determining the "optimal" grid size
based on the sparsity of the points in the input dataset.
Returns
-------
ImageData
A uniform grid with the specified dimensions that surrounds the input
dataset.
"""
bounds = np.array(dataset.bounds)
if dimensions is None:
# TODO: we should implement an algorithm to automatically determine an
# "optimal" grid size by looking at the sparsity of the points in the
# input dataset - I actually think VTK might have this implemented
# somewhere
msg = 'Please specify dimensions.'
raise NotImplementedError(msg)
dimensions = np.array(dimensions, dtype=int)
image = pyvista.ImageData()
image.dimensions = dimensions
dims = dimensions - 1
dims[dims == 0] = 1
image.spacing = (bounds[1::2] - bounds[:-1:2]) / dims
image.origin = bounds[::2]
return image
def grid_from_sph_coords(theta, phi, r):
"""Create a structured grid from arrays of spherical coordinates.
Parameters
----------
theta : array_like[float]
Azimuthal angle in degrees ``[0, 360]``.
phi : array_like[float]
Polar (zenith) angle in degrees ``[0, 180]``.
r : array_like[float]
Distance (radius) from the point of origin.
Returns
-------
pyvista.StructuredGrid
Structured grid.
See Also
--------
:ref:`spherical_example`
"""
x, y, z = np.meshgrid(np.radians(theta), np.radians(phi), r)
# Transform grid to cartesian coordinates
x_cart = z * np.sin(y) * np.cos(x)
y_cart = z * np.sin(y) * np.sin(x)
z_cart = z * np.cos(y)
# Make a grid object
return pyvista.StructuredGrid(x_cart, y_cart, z_cart)
@_deprecate_positional_args
def transform_vectors_sph_to_cart(theta, phi, r, u, v, w): # noqa: PLR0917 # numpydoc ignore=RT02
"""Transform vectors from spherical (r, phi, theta) to cartesian coordinates (z, y, x).
Note the "reverse" order of arrays's axes, commonly used in geosciences.
Parameters
----------
theta : array_like[float]
Azimuthal angle in degrees ``[0, 360]`` of shape ``(M,)``.
phi : array_like[float]
Polar (zenith) angle in degrees ``[0, 180]`` of shape ``(N,)``.
r : array_like[float]
Distance (radius) from the point of origin of shape ``(P,)``.
u : array_like[float]
X-component of the vector of shape ``(P, N, M)``.
v : array_like[float]
Y-component of the vector of shape ``(P, N, M)``.
w : array_like[float]
Z-component of the vector of shape ``(P, N, M)``.
Returns
-------
u_t, v_t, w_t : :class:`numpy.ndarray`
Arrays of transformed x-, y-, z-components, respectively.
"""
xx, yy, _ = np.meshgrid(np.radians(theta), np.radians(phi), r, indexing='ij')
th, ph = xx.squeeze(), yy.squeeze()
# Transform wind components from spherical to cartesian coordinates
# https://en.wikipedia.org/wiki/Vector_fields_in_cylindrical_and_spherical_coordinates
u_t = np.sin(ph) * np.cos(th) * w + np.cos(ph) * np.cos(th) * v - np.sin(th) * u
v_t = np.sin(ph) * np.sin(th) * w + np.cos(ph) * np.sin(th) * v + np.cos(th) * u
w_t = np.cos(ph) * w - np.sin(ph) * v
return u_t, v_t, w_t
def cartesian_to_spherical(x, y, z):
"""Convert 3D Cartesian coordinates to spherical coordinates.
Parameters
----------
x, y, z : numpy.ndarray
Cartesian coordinates.
Returns
-------
r : numpy.ndarray
Radial distance.
phi : numpy.ndarray
Angle (radians) with respect to the polar axis. Also known
as polar angle.
theta : numpy.ndarray
Angle (radians) of rotation from the initial meridian plane.
Also known as azimuthal angle.
Examples
--------
>>> import numpy as np
>>> import pyvista as pv
>>> grid = pv.ImageData(dimensions=(3, 3, 3))
>>> x, y, z = grid.points.T
>>> r, phi, theta = pv.cartesian_to_spherical(x, y, z)
"""
xy2 = x**2 + y**2
r = np.sqrt(xy2 + z**2)
phi = np.arctan2(np.sqrt(xy2), z) # the polar angle in radian angles
theta = np.arctan2(y, x) # the azimuth angle in radian angles
return r, phi, theta
def spherical_to_cartesian(r, phi, theta):
"""Convert Spherical coordinates to 3D Cartesian coordinates.
Parameters
----------
r : numpy.ndarray
Radial distance.
phi : numpy.ndarray
Angle (radians) with respect to the polar axis. Also known
as polar angle.
theta : numpy.ndarray
Angle (radians) of rotation from the initial meridian plane.
Also known as azimuthal angle.
Returns
-------
numpy.ndarray, numpy.ndarray, numpy.ndarray
Cartesian coordinates.
"""
s = np.sin(phi)
x = r * s * np.cos(theta)
y = r * s * np.sin(theta)
z = r * np.cos(phi)
return x, y, z
@_deprecate_positional_args(allowed=['datasets'])
def merge( # noqa: PLR0917
datasets,
merge_points: bool = True, # noqa: FBT001, FBT002
main_has_priority: bool | None = None, # noqa: FBT001
progress_bar: bool = False, # noqa: FBT001, FBT002
):
"""Merge several datasets.
.. note::
The behavior of this filter varies from the
:func:`PolyDataFilters.boolean_union` filter. This filter
does not attempt to create a manifold mesh and will include
internal surfaces when two meshes overlap.
.. warning::
The merge order of this filter depends on the installed version
of VTK. For example, if merging meshes ``a``, ``b``, and ``c``,
the merged order is ``bca`` for VTK<9.5 and ``abc`` for VTK>=9.5.
This may be a breaking change for some applications. If only
merging two meshes, it may be possible to maintain `some` backwards
compatibility by swapping the input order of the two meshes,
though this may also affect the merged arrays and is therefore
not fully backwards-compatible.
Parameters
----------
datasets : sequence[:class:`pyvista.DataSet`]
Sequence of datasets. Can be of any :class:`pyvista.DataSet`.
merge_points : bool, default: True
Merge equivalent points when ``True``.
main_has_priority : bool, default: True
When this parameter is ``True`` and ``merge_points=True``, the arrays
of the merging grids will be overwritten by the original main mesh.
.. deprecated:: 0.46
This keyword will be removed in a future version. The main mesh
always has priority with VTK 9.5.0 or later.
progress_bar : bool, default: False
Display a progress bar to indicate progress.
Returns
-------
pyvista.DataSet
:class:`pyvista.PolyData` if all items in datasets are
:class:`pyvista.PolyData`, otherwise returns a
:class:`pyvista.UnstructuredGrid`.
Examples
--------
Merge two polydata datasets.
>>> import pyvista as pv
>>> sphere = pv.Sphere(center=(0, 0, 1))
>>> cube = pv.Cube()
>>> mesh = pv.merge([cube, sphere])
>>> mesh.plot()
"""
if not isinstance(datasets, Sequence):
msg = f'Expected a sequence, got {type(datasets).__name__}'
raise TypeError(msg)
if len(datasets) < 1:
msg = 'Expected at least one dataset.'
raise ValueError(msg)
first = datasets[0]
if not isinstance(first, pyvista.DataSet):
msg = f'Expected pyvista.DataSet, not {type(first).__name__}'
raise TypeError(msg)
return datasets[0].merge(
datasets[1:],
merge_points=merge_points,
main_has_priority=main_has_priority,
progress_bar=progress_bar,
)
def perlin_noise(amplitude, freq: Sequence[float], phase: Sequence[float]):
"""Return the implicit function that implements Perlin noise.
Uses :vtk:`vtkPerlinNoise` and computes a Perlin noise field as
an implicit function. :vtk:`vtkPerlinNoise` is a concrete
implementation of :vtk:`vtkImplicitFunction`. Perlin noise,
originally described by Ken Perlin, is a non-periodic and
continuous noise function useful for modeling real-world objects.
The amplitude and frequency of the noise pattern are
adjustable. This implementation of Perlin noise is derived closely
from Greg Ward's version in Graphics Gems II.
Parameters
----------
amplitude : float
Amplitude of the noise function.
``amplitude`` can be negative. The noise function varies
randomly between ``-|Amplitude|`` and
``|Amplitude|``. Therefore the range of values is
``2*|Amplitude|`` large. The initial amplitude is 1.
freq : sequence[float]
The frequency, or physical scale, of the noise function
(higher is finer scale).
The frequency can be adjusted per axis, or the same for all axes.
phase : sequence[float]
Set/get the phase of the noise function.
This parameter can be used to shift the noise function within
space (perhaps to avoid a beat with a noise pattern at another
scale). Phase tends to repeat about every unit, so a phase of
0.5 is a half-cycle shift.
Returns
-------
:vtk:`vtkPerlinNoise`
Instance of :vtk:`vtkPerlinNoise` to a Perlin noise field as an
implicit function. Use with :func:`~pyvista.sample_function`.
See Also
--------
:ref:`perlin_noise_2d_example`
:ref:`perlin_noise_3d_example`
Examples
--------
Create a Perlin noise function with an amplitude of 0.1, frequency
for all axes of 1, and a phase of 0 for all axes.
>>> import pyvista as pv
>>> noise = pv.perlin_noise(0.1, (1, 1, 1), (0, 0, 0))
Sample Perlin noise over a structured grid and plot it.
>>> grid = pv.sample_function(noise, bounds=[0, 5, 0, 5, 0, 5])
>>> grid.plot()
"""
noise = _vtk.vtkPerlinNoise()
noise.SetAmplitude(amplitude)
noise.SetFrequency(freq)
noise.SetPhase(phase)
return noise
@_deprecate_positional_args(allowed=['function'])
def sample_function( # noqa: PLR0917
function: _vtk.vtkImplicitFunction,
bounds: Sequence[float] = (-1.0, 1.0, -1.0, 1.0, -1.0, 1.0),
dim: Sequence[int] = (50, 50, 50),
compute_normals: bool = False, # noqa: FBT001, FBT002
output_type: np.dtype = np.double, # type: ignore[assignment]
capping: bool = False, # noqa: FBT001, FBT002
cap_value: float = sys.float_info.max,
scalar_arr_name: str = 'scalars',
normal_arr_name: str = 'normals',
progress_bar: bool = False, # noqa: FBT001, FBT002
):
"""Sample an implicit function over a structured point set.
Uses :vtk:`vtkSampleFunction`
This method evaluates an implicit function and normals at each
point in a :vtk:`vtkStructuredPoints`. The user can specify the
sample dimensions and location in space to perform the sampling.
To create closed surfaces (in conjunction with the
:vtk:`vtkContourFilter`), capping can be turned on to set a particular
value on the boundaries of the sample space.
Parameters
----------
function : :vtk:`vtkImplicitFunction`
Implicit function to evaluate. For example, the function
generated from :func:`perlin_noise() <pyvista.core.utilities.features.perlin_noise>`.
bounds : sequence[float], default: (-1.0, 1.0, -1.0, 1.0, -1.0, 1.0)
Specify the bounds in the format of:
- ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
dim : sequence[float], default: (50, 50, 50)
Dimensions of the data on which to sample in the format of
``(xdim, ydim, zdim)``.
compute_normals : bool, default: False
Enable or disable the computation of normals.
output_type : numpy.dtype, default: numpy.double
Set the output scalar type. One of the following:
- ``np.float64``
- ``np.float32``
- ``np.int64``
- ``np.uint64``
- ``np.int32``
- ``np.uint32``
- ``np.int16``
- ``np.uint16``
- ``np.int8``
- ``np.uint8``
capping : bool, default: False
Enable or disable capping. If capping is enabled, then the outer
boundaries of the structured point set are set to cap value. This can
be used to ensure surfaces are closed.
cap_value : float, default: sys.float_info.max
Capping value used with the ``capping`` parameter.
scalar_arr_name : str, default: "scalars"
Set the scalar array name for this data set.
normal_arr_name : str, default: "normals"
Set the normal array name for this data set.
progress_bar : bool, default: False
Display a progress bar to indicate progress.
Returns
-------
pyvista.ImageData
Uniform grid with sampled data.
Examples
--------
Sample Perlin noise over a structured grid in 3D.
>>> import pyvista as pv
>>> noise = pv.perlin_noise(0.1, (1, 1, 1), (0, 0, 0))
>>> grid = pv.sample_function(
... noise, bounds=[0, 3.0, -0, 1.0, 0, 1.0], dim=(60, 20, 20)
... )
>>> grid.plot(cmap='gist_earth_r', show_scalar_bar=False, show_edges=True)
Sample Perlin noise in 2D and plot it.
>>> noise = pv.perlin_noise(0.1, (5, 5, 5), (0, 0, 0))
>>> surf = pv.sample_function(noise, dim=(200, 200, 1))
>>> surf.plot()
See :ref:`perlin_noise_2d_example` and :ref:`perlin_noise_3d_example`
for a full example using this function.
"""
# internal import to avoide circular dependency
from pyvista.core.filters import _update_alg # noqa: PLC0415
samp = _vtk.vtkSampleFunction()
samp.SetImplicitFunction(function)
samp.SetSampleDimensions(dim) # type: ignore[call-overload]
samp.SetModelBounds(bounds)
samp.SetComputeNormals(compute_normals)
samp.SetCapping(capping)
samp.SetCapValue(cap_value)
samp.SetNormalArrayName(normal_arr_name)
samp.SetScalarArrayName(scalar_arr_name)
if output_type == np.float64:
samp.SetOutputScalarTypeToDouble()
elif output_type == np.float32:
samp.SetOutputScalarTypeToFloat()
elif output_type == np.int64:
if os.name == 'nt':
msg = 'This function on Windows only supports int32 or smaller'
raise ValueError(msg)
samp.SetOutputScalarTypeToLong()
elif output_type == np.uint64:
if os.name == 'nt':
msg = 'This function on Windows only supports int32 or smaller'
raise ValueError(msg)
samp.SetOutputScalarTypeToUnsignedLong()
elif output_type == np.int32:
samp.SetOutputScalarTypeToInt()
elif output_type == np.uint32:
samp.SetOutputScalarTypeToUnsignedInt()
elif output_type == np.int16:
samp.SetOutputScalarTypeToShort()
elif output_type == np.uint16:
samp.SetOutputScalarTypeToUnsignedShort()
elif output_type == np.int8:
samp.SetOutputScalarTypeToChar()
elif output_type == np.uint8:
samp.SetOutputScalarTypeToUnsignedChar()
else:
msg = f'Invalid output_type {output_type}'
raise ValueError(msg)
_update_alg(samp, progress_bar=progress_bar, message='Sampling')
return wrap(samp.GetOutput())
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,398 @@
"""Core helper utilities."""
from __future__ import annotations
from collections import deque
from collections.abc import Sequence
from typing import TYPE_CHECKING
from typing import Any
from typing import cast
from typing import overload
import numpy as np
from typing_extensions import TypeIs
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _validation
from pyvista.core import _vtk_core as _vtk
from . import transformations
from .fileio import from_meshio
from .fileio import is_meshio_mesh
if TYPE_CHECKING:
from meshio import Mesh
from trimesh import Trimesh
from pyvista import DataObject
from pyvista import DataSet
from pyvista import ExplicitStructuredGrid
from pyvista import ImageData
from pyvista import MultiBlock
from pyvista import PartitionedDataSet
from pyvista import PointSet
from pyvista import PolyData
from pyvista import RectilinearGrid
from pyvista import StructuredGrid
from pyvista import Table
from pyvista import UnstructuredGrid
from pyvista import pyvista_ndarray
from pyvista.core._typing_core import NumpyArray
from pyvista.core._typing_core import VectorLike
from pyvista.wrappers import _WrappableVTKDataObjectType
# vtkDataSet overloads
# Overload types should match the mappings in the `pyvista._wrappers` dict
# Overloads should be ordered from narrow types (child class) to general types (parent class)
@overload
def wrap(dataset: _vtk.vtkPolyData) -> PolyData: ... # type: ignore[overload-overlap]
@overload
def wrap(dataset: _vtk.vtkStructuredGrid) -> StructuredGrid: ... # type: ignore[overload-overlap]
@overload
def wrap(dataset: _vtk.vtkExplicitStructuredGrid) -> ExplicitStructuredGrid: ... # type: ignore[overload-overlap]
@overload
def wrap(dataset: _vtk.vtkUnstructuredGrid) -> UnstructuredGrid: ... # type: ignore[overload-overlap]
@overload
def wrap(dataset: _vtk.vtkPointSet) -> PointSet: ...
@overload
def wrap(dataset: _vtk.vtkRectilinearGrid) -> RectilinearGrid: ...
@overload
def wrap(dataset: _vtk.vtkStructuredPoints) -> ImageData: ...
@overload
def wrap(dataset: _vtk.vtkImageData) -> ImageData: ...
@overload
def wrap(dataset: _vtk.vtkMultiBlockDataSet) -> MultiBlock: ...
@overload
def wrap(dataset: _vtk.vtkTable) -> Table: ...
@overload
def wrap(dataset: _vtk.vtkPartitionedDataSet) -> PartitionedDataSet: ...
# General catch-all cases
@overload
def wrap(dataset: _vtk.vtkDataSet) -> DataSet: ...
@overload
def wrap(dataset: _vtk.vtkDataObject) -> DataObject: ...
# Misc overloads
@overload
def wrap(dataset: NumpyArray[float]) -> PolyData | ImageData: ...
@overload
def wrap(dataset: _vtk.vtkAbstractArray) -> pyvista_ndarray: ...
@overload
def wrap(dataset: None) -> None: ...
# Third-party meshes
@overload
def wrap(dataset: Trimesh) -> PolyData: ...
# TODO: Support meshio overload
# @overload
# def wrap(dataset: Mesh) -> UnstructuredGrid: ...
def wrap( # noqa: PLR0911
dataset: _WrappableVTKDataObjectType
| DataObject
| Trimesh
| Mesh
| _vtk.vtkAbstractArray
| NumpyArray[float]
| None,
) -> DataObject | pyvista_ndarray | None:
"""Wrap any given VTK data object to its appropriate PyVista data object.
Other formats that are supported include:
* 2D :class:`numpy.ndarray` of XYZ vertices
* 3D :class:`numpy.ndarray` representing a volume. Values will be scalars.
* 3D :class:`trimesh.Trimesh` mesh.
* 3D :class:`meshio.Mesh` mesh.
.. versionchanged:: 0.38.0
If the passed object is already a wrapped PyVista object, then
this is no-op and will return that object directly. In previous
versions of PyVista, this would perform a shallow copy.
Parameters
----------
dataset : :class:`numpy.ndarray` | :class:`trimesh.Trimesh` | vtk.DataSet
Dataset to wrap.
Returns
-------
pyvista.DataSet
The PyVista wrapped dataset.
See Also
--------
:ref:`wrap_trimesh_example`
Examples
--------
Wrap a numpy array representing a random point cloud.
>>> import numpy as np
>>> import pyvista as pv
>>> points = np.random.default_rng().random((10, 3))
>>> cloud = pv.wrap(points)
>>> cloud
PolyData (...)
N Cells: 10
N Points: 10
N Strips: 0
X Bounds: ...
Y Bounds: ...
Z Bounds: ...
N Arrays: 0
Wrap a VTK object.
>>> import pyvista as pv
>>> import vtk
>>> points = vtk.vtkPoints()
>>> p = [1.0, 2.0, 3.0]
>>> vertices = vtk.vtkCellArray()
>>> pid = points.InsertNextPoint(p)
>>> _ = vertices.InsertNextCell(1)
>>> _ = vertices.InsertCellPoint(pid)
>>> point = vtk.vtkPolyData()
>>> _ = point.SetPoints(points)
>>> _ = point.SetVerts(vertices)
>>> mesh = pv.wrap(point)
>>> mesh
PolyData (...)
N Cells: 1
N Points: 1
N Strips: 0
X Bounds: 1.000e+00, 1.000e+00
Y Bounds: 2.000e+00, 2.000e+00
Z Bounds: 3.000e+00, 3.000e+00
N Arrays: 0
Wrap a Trimesh object.
>>> import trimesh
>>> import pyvista as pv
>>> points = [[0, 0, 0], [0, 0, 1], [0, 1, 0]]
>>> faces = [[0, 1, 2]]
>>> tmesh = trimesh.Trimesh(points, faces=faces, process=False)
>>> mesh = pv.wrap(tmesh)
>>> mesh # doctest:+SKIP
PolyData (0x7fc55ff27ad0)
N Cells: 1
N Points: 3
X Bounds: 0.000e+00, 0.000e+00
Y Bounds: 0.000e+00, 1.000e+00
Z Bounds: 0.000e+00, 1.000e+00
N Arrays: 0
"""
# Return if None
if dataset is None:
return None
if isinstance(dataset, tuple(pyvista._wrappers.values())):
# Return object if it is already wrapped
return cast('DataObject', dataset)
# Check if dataset is a numpy array. We do this first since
# pyvista_ndarray contains a VTK type that we don't want to
# directly wrap.
if isinstance(dataset, (np.ndarray, pyvista.pyvista_ndarray)):
if dataset.ndim == 1 and dataset.shape[0] == 3:
return pyvista.PolyData(dataset)
if dataset.ndim > 1 and dataset.ndim < 3 and dataset.shape[1] == 3:
return pyvista.PolyData(dataset)
elif dataset.ndim == 3:
mesh = pyvista.ImageData(dimensions=dataset.shape)
if isinstance(dataset, pyvista.pyvista_ndarray):
# this gets rid of pesky VTK reference since we're raveling this
dataset = np.asarray(dataset)
mesh['values'] = dataset.ravel(order='F')
mesh.active_scalars_name = 'values'
return mesh
else:
msg = 'NumPy array could not be wrapped pyvista.'
raise NotImplementedError(msg)
# wrap VTK arrays as pyvista_ndarray
if isinstance(dataset, _vtk.vtkDataArray):
return pyvista.pyvista_ndarray(dataset)
# Check if a dataset is a VTK type
if hasattr(dataset, 'GetClassName'):
key = dataset.GetClassName()
try:
return pyvista._wrappers[key](dataset)
except KeyError:
msg = f'VTK data type ({key}) is not currently supported by pyvista.'
raise TypeError(msg)
# wrap meshio
if is_meshio_mesh(dataset):
return from_meshio(dataset)
# wrap trimesh
if dataset.__class__.__name__ == 'Trimesh':
# trimesh doesn't pad faces
dataset = cast('Trimesh', dataset)
polydata = pyvista.PolyData.from_regular_faces(
np.asarray(dataset.vertices),
faces=dataset.faces,
)
# If the Trimesh object has uv, pass them to the PolyData
if hasattr(dataset.visual, 'uv') and dataset.visual.uv is not None:
polydata.active_texture_coordinates = np.asarray(dataset.visual.uv)
return polydata
# otherwise, flag tell the user we can't wrap this object
msg = f'Unable to wrap ({type(dataset)}) into a pyvista type.'
raise NotImplementedError(msg)
def is_pyvista_dataset(obj: Any) -> TypeIs[pyvista.DataSet | pyvista.MultiBlock]:
"""Return ``True`` if the object is a PyVista wrapped dataset.
Parameters
----------
obj : Any
Any object to test.
Returns
-------
bool
``True`` when the object is a :class:`pyvista.DataSet`.
"""
return isinstance(obj, (pyvista.DataSet, pyvista.MultiBlock))
def generate_plane(normal: VectorLike[float], origin: VectorLike[float]):
"""Return a :vtk:`vtkPlane`.
Parameters
----------
normal : sequence[float]
Three item sequence representing the normal of the plane.
origin : sequence[float]
Three item sequence representing the origin of the plane.
Returns
-------
:vtk:`vtkPlane`
VTK plane.
"""
plane = _vtk.vtkPlane()
# NORMAL MUST HAVE MAGNITUDE OF 1
normal_ = _validation.validate_array3(normal, dtype_out=float)
normal_ = normal_ / np.linalg.norm(normal_)
plane.SetNormal(*normal_)
plane.SetOrigin(*origin)
return plane
@_deprecate_positional_args(allowed=['points', 'angle'])
def axis_rotation( # noqa: PLR0917
points: NumpyArray[float],
angle: float,
inplace: bool = False, # noqa: FBT001, FBT002
deg: bool = True, # noqa: FBT001, FBT002
axis='z',
):
"""Rotate points by angle about an axis.
Parameters
----------
points : numpy.ndarray
Array of points with shape ``(N, 3)``.
angle : float
Rotation angle.
inplace : bool, default: False
Updates points in-place while returning nothing.
deg : bool, default: True
If ``True``, the angle is interpreted as degrees instead of
radians.
axis : str, default: "z"
Name of axis to rotate about. Valid options are ``'x'``, ``'y'``,
and ``'z'``.
Returns
-------
numpy.ndarray
Rotated points.
Examples
--------
Rotate a set of points by 90 degrees about the x-axis in-place.
>>> import numpy as np
>>> import pyvista as pv
>>> from pyvista import examples
>>> points = examples.load_airplane().points
>>> points_orig = points.copy()
>>> pv.axis_rotation(points, 90, axis='x', deg=True, inplace=True)
>>> assert np.all(np.isclose(points[:, 0], points_orig[:, 0]))
>>> assert np.all(np.isclose(points[:, 1], -points_orig[:, 2]))
>>> assert np.all(np.isclose(points[:, 2], points_orig[:, 1]))
"""
axis = axis.lower()
axis_to_vec = {'x': (1, 0, 0), 'y': (0, 1, 0), 'z': (0, 0, 1)}
if axis not in axis_to_vec:
msg = 'Invalid axis. Must be either "x", "y", or "z"'
raise ValueError(msg)
rot_mat = transformations.axis_angle_rotation(axis_to_vec[axis], angle, deg=deg)
return transformations.apply_transformation_to_points(rot_mat, points, inplace=inplace)
def is_inside_bounds(point, bounds):
"""Check if a point is inside a set of bounds.
This is implemented through recursion so that this is N-dimensional.
Parameters
----------
point : sequence[float]
Three item cartesian point (i.e. ``[x, y, z]``).
bounds : sequence[float]
Six item bounds in the form of ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
Returns
-------
bool
``True`` when ``point`` is inside ``bounds``.
"""
if isinstance(point, (int, float)):
point = [point]
if isinstance(point, (np.ndarray, Sequence)) and not isinstance(
point,
deque,
):
if len(bounds) < 2 * len(point) or len(bounds) % 2 != 0:
msg = 'Bounds mismatch point dimensionality'
raise ValueError(msg)
point = deque(point)
bounds = deque(bounds)
return is_inside_bounds(point, bounds)
if not isinstance(point, deque):
msg = f'Unknown input data type ({type(point)}).'
raise TypeError(msg)
if len(point) < 1:
return True
p = point.popleft()
lower, upper = bounds.popleft(), bounds.popleft()
if lower <= p <= upper:
return is_inside_bounds(point, bounds)
return False
@@ -0,0 +1,879 @@
"""Provide sources for generating images."""
from __future__ import annotations
from typing import TYPE_CHECKING
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.utilities.misc import _NoNewAttrMixin
from .helpers import wrap
if TYPE_CHECKING:
from collections.abc import Sequence
class ImageEllipsoidSource(
_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkImageEllipsoidSource
):
"""Create a binary image of an ellipsoid class.
.. versionadded:: 0.44.0
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
center : sequence[float]
The center of the ellipsoid.
radius : tuple
The radius of the ellipsoid.
Examples
--------
Create an image of an ellipsoid.
>>> import pyvista as pv
>>> source = pv.ImageEllipsoidSource(
... whole_extent=(0, 20, 0, 20, 0, 0),
... center=(10, 10, 0),
... radius=(3, 4, 5),
... )
>>> source.output.plot(cpos='xy')
"""
def __init__(self, whole_extent=None, center=None, radius=None) -> None:
super().__init__()
if whole_extent is not None:
self.whole_extent = whole_extent
if center is not None:
self.center = center
if radius is not None:
self.radius = radius
@property
def whole_extent(self) -> Sequence[int]:
"""Get extent of the whole output image.
Returns
-------
sequence[int]
The extent of the whole output image.
"""
return self.GetWholeExtent()
@whole_extent.setter
def whole_extent(self, whole_extent: Sequence[int]) -> None:
"""Set extent of the whole output image.
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
"""
self.SetWholeExtent(whole_extent) # type: ignore[call-overload]
@property
def center(self) -> tuple[float, float, float]:
"""Get the center of the ellipsoid.
Returns
-------
tuple[float, float, float]
The center of the ellipsoid.
"""
return self.GetCenter()
@center.setter
def center(self, center: Sequence[float]) -> None:
"""Set the center of the ellipsoid.
Parameters
----------
center : sequence[float]
The center of the ellipsoid.
"""
self.SetCenter(center)
@property
def radius(self) -> Sequence[float]:
"""Get the radius of the ellipsoid.
Returns
-------
sequence[float]
The radius of the ellipsoid.
"""
return self.GetRadius()
@radius.setter
def radius(self, radius: Sequence[float]) -> None:
"""Set the radius of the ellipsoid.
Parameters
----------
radius : sequence[float]
The radius of the ellipsoid.
"""
self.SetRadius(radius)
@property
def output(self):
"""Get the output image as a ImageData.
Returns
-------
pyvista.ImageData
The output image.
"""
self.Update()
return wrap(self.GetOutput())
class ImageMandelbrotSource(
_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkImageMandelbrotSource
):
"""Create an image of the Mandelbrot set.
.. versionadded:: 0.44.0
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
maxiter : int
The maximum number of iterations.
Examples
--------
Create an image of the Mandelbrot set.
>>> import pyvista as pv
>>> source = pv.ImageMandelbrotSource(
... whole_extent=(0, 200, 0, 200, 0, 0),
... maxiter=100,
... )
>>> source.output.plot(cpos='xy')
"""
def __init__(self, whole_extent=None, maxiter=None) -> None:
super().__init__()
if whole_extent is not None:
self.whole_extent = whole_extent
if maxiter is not None:
self.maxiter = maxiter
@property
def whole_extent(self) -> Sequence[int]:
"""Get extent of the whole output image.
Returns
-------
sequence[int]
The extent of the whole output image.
"""
return self.GetWholeExtent()
@whole_extent.setter
def whole_extent(self, whole_extent: Sequence[int]) -> None:
"""Set extent of the whole output image.
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
"""
self.SetWholeExtent(whole_extent) # type: ignore[call-overload]
@property
def maxiter(self) -> int:
"""Get the maximum number of iterations.
Returns
-------
int
The maximum number of iterations.
"""
return self.GetMaximumNumberOfIterations()
@maxiter.setter
def maxiter(self, maxiter: int) -> None:
"""Set the maximum number of iterations.
Parameters
----------
maxiter : int
The maximum number of iterations.
"""
self.SetMaximumNumberOfIterations(maxiter)
@property
def output(self):
"""Get the output image as a ImageData.
Returns
-------
pyvista.ImageData
The output image.
"""
self.Update()
return wrap(self.GetOutput())
class ImageNoiseSource(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkImageNoiseSource):
"""Create an image filled with uniform noise.
.. versionadded:: 0.44.0
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
minimum : float
The minimum value for the generated noise.
maximum : float
The maximum value for the generated noise.
seed : int, optional
Seed the random number generator with a value.
Examples
--------
Create an image of noise.
>>> import pyvista as pv
>>> source = pv.ImageNoiseSource(
... whole_extent=(0, 200, 0, 200, 0, 0),
... minimum=0,
... maximum=255,
... seed=0,
... )
>>> source.output.plot(cpos='xy')
"""
@_deprecate_positional_args
def __init__( # noqa: PLR0917
self,
whole_extent=(0, 255, 0, 255, 0, 0),
minimum=0.0,
maximum=1.0,
seed=None,
) -> None:
super().__init__()
if whole_extent is not None:
self.whole_extent = whole_extent
if minimum is not None:
self.minimum = minimum
if maximum is not None:
self.maximum = maximum
if seed is not None:
self.seed(seed)
@property
def whole_extent(self) -> Sequence[int]:
"""Get extent of the whole output image.
Returns
-------
sequence[int]
The extent of the whole output image.
"""
return self._whole_extent
@whole_extent.setter
def whole_extent(self, whole_extent: Sequence[int]) -> None:
"""Set extent of the whole output image.
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
"""
self._whole_extent = whole_extent
self.SetWholeExtent(whole_extent)
@property
def minimum(self) -> float:
"""Get the minimum value for the generated noise.
Returns
-------
float
The minimum value for the generated noise.
"""
return self.GetMinimum()
@minimum.setter
def minimum(self, minimum: float) -> None:
"""Set the minimum value for the generated noise.
Parameters
----------
minimum : float
The minimum value for the generated noise.
"""
self.SetMinimum(minimum)
@property
def maximum(self) -> float:
"""Get the maximum value for the generated noise.
Returns
-------
float
The maximum value for the generated noise.
"""
return self.GetMaximum()
@maximum.setter
def maximum(self, maximum: float) -> None:
"""Set the maximum value for the generated noise.
Parameters
----------
maximum : float
The maximum value for the generated noise.
"""
self.SetMaximum(maximum)
def seed(self, value: int) -> None:
"""Seed the random number generator with a value.
Parameters
----------
value : int
The seed value for the random number generator to use.
"""
_vtk.vtkMath().RandomSeed(value)
@property
def output(self):
"""Get the output image as a ImageData.
Returns
-------
pyvista.ImageData
The output image.
"""
self.Update()
return wrap(self.GetOutput())
class ImageSinusoidSource(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkImageSinusoidSource):
"""Create an image of a sinusoid.
.. versionadded:: 0.44.0
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
direction : tuple
The direction vector which determines the sinusoidal orientation.
period : float
The period of the sinusoid in pixel.
phase : tuple
The phase of the sinusoid in pixel.
amplitude : float
The magnitude of the sinusoid.
Examples
--------
Create an image of a sinusoid.
>>> import pyvista as pv
>>> source = pv.ImageSinusoidSource(
... whole_extent=(0, 200, 0, 200, 0, 0),
... period=20.0,
... phase=0.0,
... amplitude=255,
... direction=(1.0, 0.0, 0.0),
... )
>>> source.output.plot(cpos='xy')
"""
@_deprecate_positional_args
def __init__( # noqa: PLR0917
self,
whole_extent=None,
direction=None,
period=None,
phase=None,
amplitude=None,
) -> None:
super().__init__()
if whole_extent is not None:
self.whole_extent = whole_extent
if direction is not None:
self.direction = direction
if period is not None:
self.period = period
if phase is not None:
self.phase = phase
if amplitude is not None:
self.amplitude = amplitude
@property
def whole_extent(self) -> Sequence[int]:
"""Get extent of the whole output image.
Returns
-------
sequence[int]
The extent of the whole output image.
"""
return self._whole_extent
@whole_extent.setter
def whole_extent(self, whole_extent: Sequence[int]) -> None:
"""Set extent of the whole output image.
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
"""
self._whole_extent = whole_extent
self.SetWholeExtent(
whole_extent[0],
whole_extent[1],
whole_extent[2],
whole_extent[3],
whole_extent[4],
whole_extent[5],
)
@property
def direction(self) -> Sequence[float]:
"""Get the direction of the sinusoid.
Returns
-------
sequence[float]
The direction of the sinusoid.
"""
return self.GetDirection()
@direction.setter
def direction(self, direction: Sequence[float]) -> None:
"""Set the direction of the sinusoid.
Parameters
----------
direction : sequence[float]
The direction of the sinusoid.
"""
self.SetDirection(direction) # type: ignore[call-overload]
@property
def period(self) -> float:
"""Get the period of the sinusoid.
Returns
-------
float
The period of the sinusoid in pixel.
"""
return self.GetPeriod()
@period.setter
def period(self, period: float) -> None:
"""Set the period of the sinusoid.
Parameters
----------
period : float
The period of the sinusoid in pixel.
"""
self.SetPeriod(period)
@property
def phase(self) -> Sequence[float]:
"""Get the phase of the sinusoid.
Returns
-------
sequence[float]
The phase of the sinusoid in pixel.
"""
return self.GetPhase() # type: ignore[return-value]
@phase.setter
def phase(self, phase: Sequence[float]) -> None:
"""Set the phase of the sinusoid.
Parameters
----------
phase : sequence[float]
The phase of the sinusoid in pixel.
"""
self.SetPhase(phase) # type: ignore[arg-type]
@property
def amplitude(self) -> float:
"""Get the magnitude of the sinusoid.
Returns
-------
float
The magnitude of the sinusoid.
"""
return self.GetAmplitude()
@amplitude.setter
def amplitude(self, amplitude: float) -> None:
"""Set the magnitude of the sinusoid.
Parameters
----------
amplitude : float
The magnitude of the sinusoid.
"""
self.SetAmplitude(amplitude)
@property
def output(self):
"""Get the output image as a ImageData.
Returns
-------
pyvista.ImageData
The output image.
"""
self.Update()
return wrap(self.GetOutput())
class ImageGaussianSource(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkImageGaussianSource):
"""Create a binary image with Gaussian pixel values.
.. versionadded:: 0.44.0
Parameters
----------
center : sequence[float]
The center of the gaussian.
whole_extent : sequence[int]
The extent of the whole output image.
maximum : float
The maximum value of the gaussian.
std : sequence[float]
The standard deviation of the gaussian.
Examples
--------
Create an image of Gaussian pixel values.
>>> import pyvista as pv
>>> source = pv.ImageGaussianSource(
... center=(100, 100, 0),
... whole_extent=(0, 200, 0, 200, 0, 0),
... maximum=255,
... std=100.0,
... )
>>> source.output.plot(cpos='xy')
"""
@_deprecate_positional_args
def __init__( # noqa: PLR0917
self, center=None, whole_extent=None, maximum=None, std=None
) -> None:
super().__init__()
if center is not None:
self.center = center
if whole_extent is not None:
self.whole_extent = whole_extent
if maximum is not None:
self.maximum = maximum
if std is not None:
self.std = std
@property
def center(self) -> tuple[float, float, float]:
"""Get the center of the gaussian.
Returns
-------
tuple[float, float, float]
The center of the gaussian.
"""
return self.GetCenter()
@center.setter
def center(self, center: Sequence[float]) -> None:
"""Set the center of the gaussian.
Parameters
----------
center : sequence[float]
The center of the gaussian.
"""
self.SetCenter(center)
@property
def whole_extent(self) -> Sequence[int]:
"""Get extent of the whole output image.
Returns
-------
sequence[int]
The extent of the whole output image.
"""
return self._whole_extent
@whole_extent.setter
def whole_extent(self, whole_extent: Sequence[int]) -> None:
"""Set extent of the whole output image.
Parameters
----------
whole_extent : sequence[int]
The extent of the whole output image.
"""
self._whole_extent = whole_extent
self.SetWholeExtent(
whole_extent[0],
whole_extent[1],
whole_extent[2],
whole_extent[3],
whole_extent[4],
whole_extent[5],
)
@property
def maximum(self) -> float:
"""Get the maximum value of the gaussian.
Returns
-------
float
The maximum value of the gaussian.
"""
return self.GetMaximum()
@maximum.setter
def maximum(self, maximum: float) -> None:
"""Set the maximum value of the gaussian.
Parameters
----------
maximum : float
The maximum value of the gaussian.
"""
self.SetMaximum(maximum)
@property
def std(self) -> float:
"""Get the standard deviation of the gaussian.
Returns
-------
float
The standard deviation of the gaussian.
"""
return self.GetStandardDeviation()
@std.setter
def std(self, std: float) -> None:
"""Set the standard deviation of the gaussian.
Parameters
----------
std : float
The standard deviation of the gaussian.
"""
self.SetStandardDeviation(std)
@property
def output(self):
"""Get the output image as a ImageData.
Returns
-------
pyvista.ImageData
The output image.
"""
self.Update()
return wrap(self.GetOutput())
class ImageGridSource(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkImageGridSource):
"""Create an image of a grid.
.. versionadded:: 0.44.0
Parameters
----------
origin : sequence[float]
The origin of the grid.
extent : sequence[int]
The extent of the whole output image, Default: (0,255,0,255,0,0).
spacing : tuple
The pixel spacing.
Examples
--------
Create an image of a grid.
>>> import pyvista as pv
>>> source = pv.ImageGridSource(
... extent=(0, 20, 0, 20, 0, 0),
... spacing=(1, 1, 1),
... )
>>> source.output.plot(cpos='xy')
"""
def __init__(self, origin=None, extent=None, spacing=None) -> None:
super().__init__()
if origin is not None:
self.origin = origin
if extent is not None:
self.extent = extent
if spacing is not None:
self.spacing = spacing
@property
def origin(self) -> Sequence[float]:
"""Get the origin of the data.
Returns
-------
sequence[float]
The origin of the grid.
"""
return self.GetGridOrigin()
@origin.setter
def origin(self, origin: Sequence[float]) -> None:
"""Set the origin of the data.
Parameters
----------
origin : sequence[float]
The origin of the grid.
"""
self.SetGridOrigin(origin) # type: ignore[arg-type]
@property
def extent(self) -> Sequence[int]:
"""Get extent of the whole output image.
Returns
-------
sequence[int]
The extent of the whole output image.
"""
return self.GetDataExtent()
@extent.setter
def extent(self, extent: Sequence[int]) -> None:
"""Set extent of the whole output image.
Parameters
----------
extent : sequence[int]
The extent of the whole output image.
"""
self.SetDataExtent(extent)
@property
def spacing(self) -> Sequence[float]:
"""Get the spacing of the grid.
Returns
-------
sequence[float]
The pixel spacing.
"""
return self.GetDataSpacing()
@spacing.setter
def spacing(self, spacing: Sequence[float]) -> None:
"""Set the spacing of the grid.
Parameters
----------
spacing : sequence[float]
The pixel spacing.
"""
self.SetDataSpacing(spacing)
@property
def output(self):
"""Get the output image as a ImageData.
Returns
-------
pyvista.ImageData
The output image.
"""
self.Update()
return wrap(self.GetOutput())
@@ -0,0 +1,479 @@
"""Miscellaneous core utilities."""
from __future__ import annotations
from abc import ABCMeta
from collections.abc import Sequence
import enum
from functools import cache
import importlib
import sys
import threading
import traceback
from typing import TYPE_CHECKING
from typing import TypeVar
import warnings
import numpy as np
from typing_extensions import Self
if TYPE_CHECKING:
from typing import Any
from pyvista._typing_core import ArrayLike
from pyvista._typing_core import NumpyArray
from pyvista._typing_core import VectorLike
_T = TypeVar('_T')
T = TypeVar('T', bound='AnnotatedIntEnum')
def assert_empty_kwargs(**kwargs) -> bool:
"""Assert that all keyword arguments have been used (internal helper).
If any keyword arguments are passed, a ``TypeError`` is raised.
Parameters
----------
**kwargs : dict
Keyword arguments passed to the function.
Returns
-------
bool
``True`` when successful.
Raises
------
TypeError
If any keyword arguments are passed, a ``TypeError`` is raised.
"""
n = len(kwargs)
if n == 0:
return True
caller = sys._getframe(1).f_code.co_name
keys = list(kwargs.keys())
bad_arguments = ', '.join([f'"{key}"' for key in keys])
grammar = 'is an invalid keyword argument' if n == 1 else 'are invalid keyword arguments'
message = f'{bad_arguments} {grammar} for `{caller}`'
raise TypeError(message)
def check_valid_vector(point: VectorLike[float], name: str = '') -> None:
"""Check if a vector contains three components.
Parameters
----------
point : VectorLike[float]
Input vector to check. Must be an iterable with exactly three components.
name : str, optional
Name to use in the error messages. If not provided, "Vector" will be used.
Raises
------
TypeError
If the input is not an iterable.
ValueError
If the input does not have exactly three components.
"""
if not isinstance(point, (Sequence, np.ndarray)):
msg = f'{name} must be a length three iterable of floats.'
raise TypeError(msg)
if len(point) != 3:
if name == '':
name = 'Vector'
msg = f'{name} must be a length three iterable of floats.'
raise ValueError(msg)
def abstract_class(cls_): # noqa: ANN001, ANN201 # numpydoc ignore=RT01
"""Decorate a class, overriding __new__.
Preventing a class from being instantiated similar to abc.ABCMeta
but does not require an abstract method.
Parameters
----------
cls_ : type
The class to be decorated as abstract.
"""
def __new__(cls, *args, **kwargs): # noqa: ANN001, ANN202, ARG001, N807
if cls is cls_:
msg = f'{cls.__name__} is an abstract class and may not be instantiated.'
raise TypeError(msg)
return super(cls_, cls).__new__(cls)
cls_.__new__ = __new__
return cls_
class AnnotatedIntEnum(int, enum.Enum):
"""Annotated enum type."""
annotation: str
def __new__(cls, value: int, annotation: str) -> Self:
"""Initialize."""
obj = int.__new__(cls, value)
obj._value_ = value
obj.annotation = annotation
return obj
@classmethod
def from_str(cls, input_str: str) -> Self:
"""Create an enum member from a string.
Parameters
----------
input_str : str
The string representation of the annotation for the enum member.
Returns
-------
AnnotatedIntEnum
The enum member with the specified annotation.
Raises
------
ValueError
If there is no enum member with the specified annotation.
"""
for value in cls:
if value.annotation.lower() == input_str.lower():
return value
msg = f'{cls.__name__} has no value matching {input_str}'
raise ValueError(msg)
@classmethod
def from_any(cls, value: AnnotatedIntEnum | int | str) -> Self:
"""Create an enum member from a string, int, etc.
Parameters
----------
value : int | str | AnnotatedIntEnum
The value used to determine the corresponding enum member.
Returns
-------
AnnotatedIntEnum
The enum member matching the specified value.
Raises
------
ValueError
If there is no enum member matching the specified value.
"""
if isinstance(value, cls):
return value
elif isinstance(value, int):
return cls(value) # type: ignore[call-arg]
elif isinstance(value, str):
return cls.from_str(value)
else:
msg = f'Invalid type {type(value)} for class {cls.__name__}.' # type: ignore[unreachable]
raise TypeError(msg)
@cache
def has_module(module_name: str) -> bool:
"""Return if a module can be imported.
Parameters
----------
module_name : str
Name of the module to check.
Returns
-------
bool
``True`` if the module can be imported, otherwise ``False``.
"""
module_spec = importlib.util.find_spec(module_name)
return module_spec is not None
def try_callback(func, *args) -> None: # noqa: ANN001
"""Wrap a given callback in a try statement.
Parameters
----------
func : callable
Callable object.
*args
Any arguments.
"""
try:
func(*args)
except Exception: # noqa: BLE001 # pragma: no cover
etype, exc, tb = sys.exc_info()
stack = traceback.extract_tb(tb)[1:]
formatted_exception = 'Encountered issue in callback (most recent call last):\n' + ''.join(
traceback.format_list(stack) + traceback.format_exception_only(etype, exc),
).rstrip('\n')
warnings.warn(formatted_exception)
def threaded(fn): # noqa: ANN001, ANN201
"""Call a function using a thread.
Parameters
----------
fn : callable
Callable object.
Returns
-------
function
Wrapped function.
"""
def wrapper(*args, **kwargs): # noqa: ANN202
thread = threading.Thread(target=fn, args=args, kwargs=kwargs)
thread.start()
return thread
return wrapper
class conditional_decorator: # noqa: N801
"""Conditional decorator for methods.
Parameters
----------
dec : callable
The decorator to be applied conditionally.
condition : bool
Condition to match. If ``True``, the decorator is applied. If
``False``, the function is returned unchanged.
"""
def __init__(self, dec, condition) -> None: # noqa: ANN001
"""Initialize."""
self.decorator = dec
self.condition = condition
def __call__(self, func): # noqa: ANN001, ANN204
"""Call the decorated function if condition is matched."""
if not self.condition:
# Return the function unchanged, not decorated.
return func
return self.decorator(func)
def _check_range(value: float, rng: Sequence[float], parm_name: str) -> None:
"""Check if a parameter is within a range."""
if value < rng[0] or value > rng[1]:
msg = (
f'The value {float(value)} for `{parm_name}` is outside the '
f'acceptable range {tuple(rng)}.'
)
raise ValueError(msg)
class _AutoFreezeMeta(type):
"""Metaclass to automatically freeze a class when called."""
def __call__(cls: type[_T], *args, **kwargs) -> _T:
obj = super().__call__(*args, **kwargs) # type: ignore[misc]
obj._no_new_attributes(cls)
return obj
class _AutoFreezeABCMeta(_AutoFreezeMeta, ABCMeta):
"""Metaclass to combine automatic attribute freezing with ABC support."""
class _NoNewAttrMixin(metaclass=_AutoFreezeABCMeta):
"""Mixin to prevent adding new attributes.
This class is mainly used to prevent users from setting the wrong attributes on an
object. It freezes the attributes when called and prevents setting new ones via
"normal" methods like ``obj.foo = 42``.
"""
def _no_new_attributes(self, this_class: type) -> None:
"""Prevent setting additional attributes."""
object.__setattr__(self, '__frozen', True)
object.__setattr__(self, '__frozen_by_class', this_class)
def __setattr__(self, key: str, value: Any) -> None:
"""Prevent adding new attributes to classes using "normal" methods."""
if not key.startswith('_'):
# Check if this class froze itself. Any frozen state already set by parent classes,
# e.g. by calling super().__init__(), will be ignored. This allows subclasses to set
# attributes during init without being affect by a parent class init.
frozen = self.__dict__.get('__frozen', False)
frozen_by = self.__dict__.get('__frozen_by_class', None)
if (
frozen
and frozen_by is type(self)
and not (key in type(self).__dict__ or hasattr(self, key))
):
from pyvista import PyVistaAttributeError # noqa: PLC0415
msg = (
f'Attribute {key!r} does not exist and cannot be added to class '
f'{self.__class__.__name__!r}\nUse `pv.set_new_attribute` to set new '
f'attributes or consider setting a private variable (with `_` prefix) instead.'
)
raise PyVistaAttributeError(msg)
object.__setattr__(self, key, value)
def set_new_attribute(obj: object, name: str, value: Any) -> None:
"""Set a new attribute for this object.
Python allows arbitrarily setting new attributes on objects at any time,
but PyVista's classes do not allow this. If an attribute is not part of
PyVista's API, an ``AttributeError`` is normally raised when attempting
to set it.
Use :func:`set_new_attribute` to override this and set a new attribute anyway.
Examples
--------
Set a new custom attribute on a mesh.
>>> import pyvista as pv
>>> mesh = pv.PolyData()
>>> pv.set_new_attribute(mesh, 'foo', 42)
>>> mesh.foo
42
.. versionadded:: 0.46
"""
if hasattr(obj, name):
from pyvista import PyVistaAttributeError # noqa: PLC0415
msg = (
f'Attribute {name!r} already exists. '
'`set_new_attribute` can only be used for setting NEW attributes.'
)
raise PyVistaAttributeError(msg)
object.__setattr__(obj, name, value)
def _reciprocal(
x: ArrayLike[float], tol: float = 1e-8, value_if_division_by_zero: float = 0.0
) -> NumpyArray[float]:
"""Compute the element-wise reciprocal and avoid division by zero.
The reciprocal of elements with an absolute value less than a
specified tolerance has the value specified by ``default_if_div_by_zero``.
Parameters
----------
x : array_like
Input array.
tol : float
Tolerance value. Values smaller than ``tol`` have a reciprocal of zero.
value_if_division_by_zero : float
Default value given to values less than ``tol``, i.e. the value given if division
by zero is detected.
Returns
-------
numpy.ndarray
Element-wise reciprocal of the input.
"""
x = np.array(x)
x = x if np.issubdtype(x.dtype, np.floating) else x.astype(float)
zero = np.abs(x) < tol
x[~zero] = np.reciprocal(x[~zero])
x[zero] = value_if_division_by_zero
return x
class _classproperty(property): # noqa: N801
"""Read-only class property decorator.
Use this decaorator as an alternative to chaining `@classmethod`
and `@property` which is deprecated.
See:
- https://docs.python.org/library/functions.html#classmethod
- https://stackoverflow.com/a/13624858
Examples
--------
>>> from pyvista.core.utilities.misc import _classproperty
>>> class Foo:
... @_classproperty
... def bar(cls): ...
"""
def __get__(self: property, owner_self: Any, owner_cls: type | None = None) -> Any:
return self.fget(owner_cls) # type: ignore[misc]
class _NameMixin:
"""Add a 'name' property to a class.
.. versionadded:: 0.45
"""
@property
def name(self) -> str: # numpydoc ignore=RT01
"""Get or set the unique name identifier used by PyVista."""
if not hasattr(self, '_name') or self._name is None:
address = (
self.GetAddressAsString('')
if hasattr(self, 'GetAddressAsString')
else hex(id(self))
)
return f'{type(self).__name__}({address})'
return self._name
@name.setter
def name(self, value: str) -> None:
if not value:
msg = 'Name must be truthy.'
raise ValueError(msg)
object.__setattr__(self, '_name', str(value))
class _BoundsSizeMixin:
@property
def bounds_size(self) -> tuple[float, float, float]:
"""Return the size of each axis of the object's bounding box.
.. versionadded:: 0.46
Returns
-------
tuple[float, float, float]
Size of each x-y-z axis.
Examples
--------
Get the size of a cube. The cube has edge lengths af ``(1.0, 1.0, 1.0)``
by default.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> mesh.bounds_size
(1.0, 1.0, 1.0)
"""
bounds = self.bounds # type: ignore[attr-defined]
return (
bounds.x_max - bounds.x_min,
bounds.y_max - bounds.y_min,
bounds.z_max - bounds.z_min,
)
@@ -0,0 +1,292 @@
"""Core error utilities."""
from __future__ import annotations
import importlib.util
import logging
from pathlib import Path
import re
import signal
import sys
import threading
import traceback
from typing import NamedTuple
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.utilities.misc import _NoNewAttrMixin
def set_error_output_file(filename):
"""Set a file to write out the VTK errors.
Parameters
----------
filename : str, Path
Path to the file to write VTK errors to.
Returns
-------
:vtk:`vtkFileOutputWindow`
VTK file output window.
:vtk:`vtkOutputWindow`
VTK output window.
"""
filename = Path(filename).expanduser().resolve()
fileOutputWindow = _vtk.vtkFileOutputWindow()
if pyvista.vtk_version_info < (9, 2, 2): # pragma no cover
fileOutputWindow.SetFileName(str(filename))
else:
fileOutputWindow.SetFileName(filename)
outputWindow = _vtk.vtkOutputWindow()
outputWindow.SetInstance(fileOutputWindow)
return fileOutputWindow, outputWindow
class VtkErrorCatcher:
"""Context manager to temporarily catch VTK errors.
Parameters
----------
raise_errors : bool, default: False
Raise a ``RuntimeError`` when a VTK error is encountered.
send_to_logging : bool, default: True
Determine whether VTK errors raised within the context should
also be sent to logging.
Examples
--------
Catch VTK errors using the context manager.
>>> import pyvista as pv
>>> with pv.VtkErrorCatcher() as error_catcher:
... sphere = pv.Sphere()
"""
@_deprecate_positional_args
def __init__(self, raise_errors: bool = False, send_to_logging: bool = True) -> None: # noqa: FBT001, FBT002
"""Initialize context manager."""
self.raise_errors = raise_errors
self.send_to_logging = send_to_logging
def __enter__(self) -> None:
"""Observe VTK string output window for errors."""
error_output = _vtk.vtkStringOutputWindow()
error_win = _vtk.vtkOutputWindow()
self._error_output_orig = error_win.GetInstance()
error_win.SetInstance(error_output)
obs = Observer(log=self.send_to_logging, store_history=True)
obs.observe(error_output)
self._observer = obs
def __exit__(self, *args):
"""Stop observing VTK string output window."""
error_win = _vtk.vtkOutputWindow()
error_win.SetInstance(self._error_output_orig)
self.events = self._observer.event_history
if self.raise_errors and self.events:
errors = [RuntimeError(f'{e.kind}: {e.alert}', e.path, e.address) for e in self.events]
raise RuntimeError(errors)
class VtkEvent(NamedTuple):
"""Named tuple to store VTK event information."""
kind: str
path: str
address: str
alert: str
class Observer(_NoNewAttrMixin):
"""A standard class for observing VTK objects."""
@_deprecate_positional_args(allowed=['event_type'])
def __init__(
self,
event_type='ErrorEvent',
log: bool = True, # noqa: FBT001, FBT002
store_history: bool = False, # noqa: FBT001, FBT002
) -> None:
"""Initialize observer."""
self.__event_occurred = False
self.__message = None
self.__message_etc = None
self.CallDataType = 'string0'
self.__observing = False
self.event_type = event_type
self.__log = log
self.store_history = store_history
self.event_history: list[VtkEvent] = []
@staticmethod
def parse_message(message): # numpydoc ignore=RT01
"""Parse the given message."""
# Message format
regex = re.compile(r'([A-Z]+):\sIn\s(.+),\sline\s.+\n\w+\s\((.+)\):\s(.+)')
try:
kind, path, address, alert = regex.findall(message)[0]
except Exception: # noqa: BLE001
return '', '', '', message
else:
return kind, path, address, alert
def log_message(self, kind, alert) -> None:
"""Parse different event types and passes them to logging."""
if kind == 'ERROR':
logging.error(alert) # noqa: LOG015
else:
logging.warning(alert) # noqa: LOG015
def __call__(self, _obj, _event, message) -> None:
"""Declare standard call function for the observer.
On an event occurrence, this function executes.
"""
try:
self.__event_occurred = True
self.__message_etc = message
kind, path, address, alert = self.parse_message(message)
self.__message = alert
if self.store_history:
self.event_history.append(VtkEvent(kind, path, address, alert))
if self.__log:
self.log_message(kind, alert)
except Exception: # noqa: BLE001 # pragma: no cover
try:
if len(message) > 120:
message = f'{message[:100]!r} ... ({len(message)} characters)'
else:
message = repr(message)
print(
f'PyVista error in handling VTK error message:\n{message}',
file=sys.__stdout__,
)
traceback.print_tb(sys.last_traceback, file=sys.__stderr__)
except Exception: # noqa: BLE001
pass
def has_event_occurred(self): # numpydoc ignore=RT01
"""Ask self if an error has occurred since last queried.
This resets the observer's status.
"""
occ = self.__event_occurred
self.__event_occurred = False
return occ
@_deprecate_positional_args
def get_message(self, etc: bool = False): # noqa: FBT001, FBT002
"""Get the last set error message.
Returns
-------
str
The last set error message.
"""
if etc:
return self.__message_etc
return self.__message
def observe(self, algorithm):
"""Make this an observer of an algorithm."""
if self.__observing:
msg = 'This error observer is already observing an algorithm.'
raise RuntimeError(msg)
if hasattr(algorithm, 'GetExecutive') and algorithm.GetExecutive() is not None:
algorithm.GetExecutive().AddObserver(self.event_type, self)
algorithm.AddObserver(self.event_type, self)
self.__observing = True
def send_errors_to_logging(): # numpydoc ignore=RT01
"""Send all VTK error/warning messages to Python's logging module."""
error_output = _vtk.vtkStringOutputWindow()
error_win = _vtk.vtkOutputWindow()
error_win.SetInstance(error_output)
obs = Observer()
return obs.observe(error_output)
class ProgressMonitor(_NoNewAttrMixin):
"""A standard class for monitoring the progress of a VTK algorithm.
This must be use in a ``with`` context and it will block keyboard
interrupts from happening until the exit event as interrupts will crash
the kernel if the VTK algorithm is still executing.
Parameters
----------
algorithm
VTK algorithm or filter.
message : str, default: ""
Message to display in the progress bar.
"""
def __init__(self, algorithm, message=''):
"""Initialize observer."""
if not importlib.util.find_spec('tqdm'):
msg = 'Please install `tqdm` to monitor algorithms.'
raise ImportError(msg)
self.event_type = _vtk.vtkCommand.ProgressEvent
self.progress = 0.0
self._last_progress = self.progress
self.algorithm = algorithm
self.message = message
self._interrupt_signal_received = False
self._old_progress = 0
self._old_handler = None
self._progress_bar = None
def handler(self, sig, frame) -> None:
"""Pass signal to custom interrupt handler."""
self._interrupt_signal_received = (sig, frame) # type: ignore[assignment]
logging.debug('SIGINT received. Delaying KeyboardInterrupt until VTK algorithm finishes.') # noqa: LOG015
def __call__(self, obj, *args) -> None: # noqa: ARG002
"""Call progress update callback.
On an event occurrence, this function executes.
"""
if self._interrupt_signal_received:
obj.AbortExecuteOn()
else:
progress = obj.GetProgress()
step = progress - self._old_progress
self._progress_bar.update(step) # type: ignore[union-attr]
self._old_progress = progress
def __enter__(self):
"""Enter event for ``with`` context."""
from tqdm import tqdm # noqa: PLC0415
# check if in main thread
if threading.current_thread().__class__.__name__ == '_MainThread':
self._old_handler = signal.signal(signal.SIGINT, self.handler)
self._progress_bar = tqdm(
total=1,
leave=True,
bar_format='{l_bar}{bar}[{elapsed}<{remaining}]',
)
self._progress_bar.set_description(self.message)
self.algorithm.AddObserver(self.event_type, self)
return self._progress_bar
def __exit__(self, *args) -> None:
"""Exit event for ``with`` context."""
self._progress_bar.total = 1 # type: ignore[union-attr]
self._progress_bar.refresh() # type: ignore[union-attr]
self._progress_bar.close() # type: ignore[union-attr]
self.algorithm.RemoveObservers(self.event_type)
if threading.current_thread().__class__.__name__ == '_MainThread':
signal.signal(signal.SIGINT, self._old_handler)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,874 @@
"""Points related utilities."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Literal
from typing import overload
import warnings
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _validation
from pyvista.core import _vtk_core as _vtk
if TYPE_CHECKING:
from pyvista import PolyData
from pyvista.core._typing_core import MatrixLike
from pyvista.core._typing_core import NumpyArray
from pyvista.core._typing_core import VectorLike
@_deprecate_positional_args(allowed=['points'])
def vtk_points( # noqa: PLR0917
points: VectorLike[float] | MatrixLike[float],
deep: bool = True, # noqa: FBT001, FBT002
force_float: bool = False, # noqa: FBT001, FBT002
allow_empty: bool = True, # noqa: FBT001, FBT002
) -> _vtk.vtkPoints:
"""Convert numpy array or array-like to a :vtk:`vtkPoints` object.
Parameters
----------
points : numpy.ndarray or sequence
Points to convert. Should be 1 or 2 dimensional. Accepts a
single point or several points.
deep : bool, default: True
Perform a deep copy of the array. Only applicable if
``points`` is a :class:`numpy.ndarray`.
force_float : bool, default: False
Casts the datatype to ``float32`` if points datatype is
non-float. Set this to ``False`` to allow non-float types,
though this may lead to truncation of intermediate floats
when transforming datasets.
allow_empty : bool, default: True
Allow ``points`` to be an empty array. If ``False``, points
must be strictly one- or two-dimensional.
.. versionadded:: 0.45
Returns
-------
:vtk:`vtkPoints`
The :vtk:`vtkPoints` object.
Examples
--------
>>> import pyvista as pv
>>> import numpy as np
>>> points = np.random.default_rng().random((10, 3))
>>> vpoints = pv.vtk_points(points)
>>> vpoints # doctest:+SKIP
(vtkmodules.vtkCommonCore.vtkPoints)0x7f0c2e26af40
"""
try:
points_ = _validation.validate_arrayNx3(points, name='points')
except ValueError as e:
if 'points has shape (0,)' in repr(e) and allow_empty:
points_ = np.empty(shape=(0, 3), dtype=np.array(points).dtype)
else:
raise
if force_float and not np.issubdtype(points_.dtype, np.floating):
warnings.warn(
'Points is not a float type. This can cause issues when '
'transforming or applying filters. Casting to '
'``np.float32``. Disable this by passing '
'``force_float=False``.',
)
points_ = points_.astype(np.float32)
# use the underlying vtk data if present to avoid memory leaks
if not deep and isinstance(points_, pyvista.pyvista_ndarray) and points_.VTKObject is not None:
vtk_object = points_.VTKObject
# we can only use the underlying data if `points` is not a slice of
# the VTK data object
if vtk_object.GetSize() == points_.size:
vtkpts = _vtk.vtkPoints()
vtkpts.SetData(points_.VTKObject)
return vtkpts
else:
deep = True
# points must be contiguous
points_ = np.require(points_, requirements=['C'])
vtkpts = _vtk.vtkPoints()
vtk_arr = _vtk.numpy_to_vtk(points_, deep=deep)
vtkpts.SetData(vtk_arr)
return vtkpts
def line_segments_from_points(points: VectorLike[float] | MatrixLike[float]) -> PolyData:
"""Generate non-connected line segments from points.
Assumes points are ordered as line segments and an even number of
points.
Parameters
----------
points : array_like[float]
Points representing line segments. An even number must be
given as every two vertices represent a single line
segment. For example, two line segments would be represented
as ``np.array([[0, 0, 0], [1, 0, 0], [1, 0, 0], [1, 1, 0]])``.
Returns
-------
pyvista.PolyData
PolyData with lines and cells.
Examples
--------
This example plots two line segments at right angles to each other.
>>> import pyvista as pv
>>> import numpy as np
>>> points = np.array([[0, 0, 0], [1, 0, 0], [1, 0, 0], [1, 1, 0]])
>>> lines = pv.line_segments_from_points(points)
>>> lines.plot()
"""
if len(points) % 2 != 0:
msg = 'An even number of points must be given to define each segment.'
raise ValueError(msg)
# Assuming ordered points, create array defining line order
n_points = len(points)
n_lines = n_points // 2
lines = np.c_[
2 * np.ones(n_lines, np.int_),
np.arange(0, n_points - 1, step=2),
np.arange(1, n_points + 1, step=2),
]
poly = pyvista.PolyData()
poly.points = points
poly.lines = lines
return poly
@_deprecate_positional_args(allowed=['points'])
def lines_from_points(
points: VectorLike[float] | MatrixLike[float],
close: bool = False, # noqa: FBT001, FBT002
) -> PolyData:
"""Make a connected line set given an array of points.
Parameters
----------
points : array_like[float]
Points representing the vertices of the connected
segments. For example, two line segments would be represented
as ``np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0]])``.
close : bool, default: False
If ``True``, close the line segments into a loop.
Returns
-------
pyvista.PolyData
PolyData with lines and cells.
Examples
--------
>>> import numpy as np
>>> import pyvista as pv
>>> points = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0]])
>>> poly = pv.lines_from_points(points)
>>> poly.plot(line_width=5)
"""
poly = pyvista.PolyData()
poly.points = points
cells = np.full((len(points) - 1, 3), 2, dtype=np.int_)
cells[:, 1] = np.arange(0, len(points) - 1, dtype=np.int_)
cells[:, 2] = np.arange(1, len(points), dtype=np.int_)
if close:
cells = np.append(cells, [[2, len(points) - 1, 0]], axis=0)
poly.lines = cells
return poly
@_deprecate_positional_args(allowed=['points'])
def fit_plane_to_points( # noqa: PLR0917
points: MatrixLike[float],
return_meta: bool = False, # noqa: FBT001, FBT002
resolution: int = 10,
init_normal: VectorLike[float] | None = None,
) -> PolyData | tuple[PolyData, float, NumpyArray[float]]:
"""Fit a plane to points using its :func:`principal_axes`.
The plane is automatically sized and oriented to fit the extents of
the points.
.. versionchanged:: 0.42.0
The generated plane is now sized and oriented to match the points.
.. versionchanged:: 0.42.0
The center of the plane (returned if ``return_meta=True``) is now
computed as the center of the generated plane mesh. In previous
versions, the center of the input points was returned.
.. versionchanged:: 0.45.0
The internal method used for fitting the plane has changed. Previously, singular
value decomposition (SVD) was used, but eigenvectors are now used instead.
See warning below.
.. warning::
The sign of the plane's normal vector prior to version 0.45 may differ
from the latest version. This may impact methods which rely on the plane's
direction. Use ``init_normal`` to control the sign explicitly.
Parameters
----------
points : array_like[float]
Size ``[N x 3]`` sequence of points to fit a plane through.
return_meta : bool, default: False
If ``True``, also returns the center and normal of the
generated plane.
resolution : int, default: 10
Number of points on the plane mesh along its edges. Specify two numbers to
set the resolution along the plane's long and short edge (respectively) or
a single number to set both edges to have the same resolution.
.. versionadded:: 0.45.0
init_normal : VectorLike[float] | str, optional
Flip the normal of the plane such that it best aligns with this vector. Can be
a vector or string specifying the axis by name (e.g. ``'x'`` or ``'-x'``, etc.).
.. versionadded:: 0.45.0
Returns
-------
pyvista.PolyData
Plane mesh.
pyvista.pyvista_ndarray
Plane center if ``return_meta=True``.
pyvista.pyvista_ndarray
Plane normal if ``return_meta=True``.
See Also
--------
fit_line_to_points
Fit a line using the first principal axis of the points.
principal_axes
Compute axes vectors which best fit a set of points.
Examples
--------
Fit a plane to a random point cloud.
>>> import pyvista as pv
>>> import numpy as np
>>> from pyvista import examples
>>>
>>> rng = np.random.default_rng(seed=0)
>>> cloud = rng.random((10, 3))
>>> cloud[:, 2] *= 0.1
>>>
>>> plane = pv.fit_plane_to_points(cloud)
Plot the point cloud and fitted plane.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(plane, style='wireframe', line_width=4)
>>> _ = pl.add_points(
... cloud,
... render_points_as_spheres=True,
... color='r',
... point_size=30,
... )
>>> pl.show()
Fit a plane to a mesh and return its metadata. Set the plane resolution to 1
so that the plane has no internal points or edges.
>>> mesh = examples.download_shark()
>>> plane, center, normal = pv.fit_plane_to_points(
... mesh.points, return_meta=True, resolution=1
... )
Plot the mesh and fitted plane.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(plane, show_edges=True, opacity=0.25)
>>> _ = pl.add_mesh(mesh, color='gray')
>>> pl.camera_position = [
... (-117, 76, 235),
... (1.69, -1.38, 0),
... (0.189, 0.957, -0.22),
... ]
>>> pl.show()
Use the metadata with :meth:`pyvista.DataObjectFilters.clip` to split the mesh into
two.
>>> first_half, second_half = mesh.clip(
... origin=center, normal=normal, return_clipped=True
... )
Plot the two halves of the clipped mesh.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(first_half, color='red')
>>> _ = pl.add_mesh(second_half, color='blue')
>>> pl.camera_position = [
... (-143, 43, 40),
... (-8.7, -11, -14),
... (0.25, 0.92, -0.29),
... ]
>>> pl.show()
Note that it is pointing in the positive z-direction.
>>> normal
pyvista_ndarray([5.2734075e-09, 6.7008443e-08, 1.0000000e+00],
dtype=float32)
Use ``init_normal`` to flip the sign and make it negative instead.
>>> _, _, normal = pv.fit_plane_to_points(
... mesh.points, return_meta=True, init_normal='-z'
... )
>>> normal
pyvista_ndarray([-5.2734155e-09, -6.7008422e-08, -1.0000000e+00],
dtype=float32)
"""
valid_resolution = _validation.validate_array(
resolution,
must_have_shape=[(), (2,)],
must_be_integer=True,
broadcast_to=(2,),
dtype_out=int,
)
i_resolution, j_resolution = valid_resolution
# Align points to the xyz-axes
aligned, matrix = pyvista.PolyData(points).align_xyz(
return_matrix=True, axis_2_direction=init_normal
)
# Fit plane to xyz-aligned mesh
i_size, j_size, _ = aligned.bounds_size
plane = pyvista.Plane(
i_size=i_size,
j_size=j_size,
i_resolution=i_resolution,
j_resolution=j_resolution,
)
# Transform plane back to input points positioning
inverse_matrix = pyvista.Transform(matrix).inverse_matrix
plane.transform(inverse_matrix, inplace=True)
if return_meta:
# Compute center and normal from the plane's points and normals
center = np.mean(plane.points, axis=0)
normal = np.mean(plane.point_normals, axis=0)
return plane, center, normal
return plane
def fit_line_to_points(
points: MatrixLike[float],
*,
resolution: int = 1,
init_direction: VectorLike[float] | None = None,
return_meta: bool = False,
) -> PolyData | tuple[PolyData, float, NumpyArray[float]]:
"""Fit a line to points using its :func:`principal_axes`.
The line is automatically sized and oriented to fit the extents of
the points.
.. versionadded:: 0.45.0
Parameters
----------
points : MatrixLike[float]
Size ``[N x 3]`` array of points to fit a line through.
resolution : int, default: 1
Number of pieces to divide the line into.
init_direction : VectorLike[float], optional
Flip the direction of the line's points such that it best aligns with this
vector. Can be a vector or string specifying the axis by name (e.g. ``'x'``
or ``'-x'``, etc.).
return_meta : bool, default: False
If ``True``, also returns the length (magnitude) and direction of the line.
See Also
--------
fit_plane_to_points
Fit a plane using the first two principal axes of the points.
principal_axes
Compute axes vectors which best fit a set of points.
Returns
-------
pyvista.PolyData
Line mesh.
float
Line length if ``return_meta=True``.
numpy.ndarray
Line direction (unit vector) if ``return_meta=True``.
Examples
--------
Download a point cloud. The points trace a path along topographical surface.
>>> import pyvista as pv
>>> from pyvista import examples
>>> mesh = examples.download_gpr_path()
Fit a line to the points and plot the result. The line of best fit is colored red.
>>> line = pv.fit_line_to_points(mesh.points)
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, color='black', line_width=10)
>>> _ = pl.add_mesh(line, color='red', line_width=5)
>>> pl.show()
Fit a line to a mesh and return the metadata.
>>> mesh = examples.download_human()
>>> line, length, direction = pv.fit_line_to_points(
... mesh.points, return_meta=True
... )
Show the length of the line.
>>> length
167.6145387467733
Plot the line as an arrow to show its direction.
>>> arrow = pv.Arrow(
... start=line.points[0],
... direction=direction,
... scale=length,
... tip_length=0.2,
... tip_radius=0.04,
... shaft_radius=0.01,
... )
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, opacity=0.5)
>>> _ = pl.add_mesh(arrow, color='red')
>>> pl.show()
Set ``init_direction`` to the positive z-axis to flip the line's direction.
>>> mesh = examples.download_human()
>>> line, length, direction = pv.fit_line_to_points(
... mesh.points, init_direction='z', return_meta=True
... )
Plot the results again with an arrow.
>>> arrow = pv.Arrow(
... start=line.points[0],
... direction=direction,
... scale=length,
... tip_length=0.2,
... tip_radius=0.04,
... shaft_radius=0.01,
... )
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, opacity=0.5)
>>> _ = pl.add_mesh(arrow, color='red')
>>> pl.show()
"""
# Align points to the xyz-axes
aligned, matrix = pyvista.PolyData(points).align_xyz(
axis_0_direction=init_direction, return_matrix=True
)
# Fit line to xyz-aligned mesh
point_a = (aligned.bounds.x_min, 0, 0)
point_b = (aligned.bounds.x_max, 0, 0)
line_mesh = pyvista.LineSource(point_a, point_b, resolution=resolution).output
# Transform line back to input points positioning
inverse_matrix = pyvista.Transform(matrix).inverse_matrix
line_mesh.transform(inverse_matrix, inplace=True)
if return_meta:
return line_mesh, line_mesh.length, matrix[0, :3]
return line_mesh
def make_tri_mesh(points: NumpyArray[float], faces: NumpyArray[int]) -> PolyData:
"""Construct a ``pyvista.PolyData`` mesh using points and faces arrays.
Construct a mesh from an Nx3 array of points and an Mx3 array of
triangle indices, resulting in a mesh with N vertices and M
triangles. This function does not require the standard VTK
"padding" column and simplifies mesh creation.
Parameters
----------
points : np.ndarray
Array of points with shape ``(N, 3)`` storing the vertices of the
triangle mesh.
faces : np.ndarray
Array of indices with shape ``(M, 3)`` containing the triangle
indices.
Returns
-------
pyvista.PolyData
PolyData instance containing the triangle mesh.
Examples
--------
This example discretizes the unit square into a triangle mesh with
nine vertices and eight faces.
>>> import numpy as np
>>> import pyvista as pv
>>> points = np.array(
... [
... [0, 0, 0],
... [0.5, 0, 0],
... [1, 0, 0],
... [0, 0.5, 0],
... [0.5, 0.5, 0],
... [1, 0.5, 0],
... [0, 1, 0],
... [0.5, 1, 0],
... [1, 1, 0],
... ]
... )
>>> faces = np.array(
... [
... [0, 1, 4],
... [4, 7, 6],
... [2, 5, 4],
... [4, 5, 8],
... [0, 4, 3],
... [3, 4, 6],
... [1, 2, 4],
... [4, 8, 7],
... ]
... )
>>> tri_mesh = pv.make_tri_mesh(points, faces)
>>> tri_mesh.plot(show_edges=True, line_width=5)
"""
if points.shape[1] != 3:
msg = 'Points array should have shape (N, 3).'
raise ValueError(msg)
if faces.ndim != 2 or faces.shape[1] != 3:
msg = 'Face array should have shape (M, 3).'
raise ValueError(msg)
cells = np.empty((faces.shape[0], 4), dtype=faces.dtype)
cells[:, 0] = 3
cells[:, 1:] = faces
return pyvista.PolyData(points, cells)
def vector_poly_data(
orig: VectorLike[float] | MatrixLike[float], vec: VectorLike[float] | MatrixLike[float]
) -> PolyData:
"""Create a pyvista.PolyData object composed of vectors.
Parameters
----------
orig : array_like[float]
Array of vector origins.
vec : array_like[float]
Array of vectors.
Returns
-------
pyvista.PolyData
Mesh containing the ``orig`` points along with the
``'vectors'`` and ``'mag'`` point arrays representing the
vectors and magnitude of the vectors at each point.
Examples
--------
Create basic vector field. This is a point cloud where each point
has a vector and magnitude attached to it.
>>> import pyvista as pv
>>> import numpy as np
>>> x, y = np.meshgrid(np.linspace(-5, 5, 10), np.linspace(-5, 5, 10))
>>> points = np.vstack((x.ravel(), y.ravel(), np.zeros(x.size))).T
>>> u = x / np.sqrt(x**2 + y**2)
>>> v = y / np.sqrt(x**2 + y**2)
>>> vectors = np.vstack((u.ravel() ** 3, v.ravel() ** 3, np.zeros(u.size))).T
>>> pdata = pv.vector_poly_data(points, vectors)
>>> pdata.point_data.keys()
['vectors', 'mag']
Convert these to arrows and plot it.
>>> pdata.glyph(orient='vectors', scale='mag').plot()
"""
# shape, dimension checking
if not isinstance(orig, np.ndarray):
orig = np.asarray(orig)
if not isinstance(vec, np.ndarray):
vec = np.asarray(vec)
if orig.ndim != 2:
orig = orig.reshape((-1, 3))
elif orig.shape[1] != 3:
msg = 'orig array must be 3D'
raise ValueError(msg)
if vec.ndim != 2:
vec = vec.reshape((-1, 3))
elif vec.shape[1] != 3:
msg = 'vec array must be 3D'
raise ValueError(msg)
# Create vtk points and cells objects
vpts = _vtk.vtkPoints()
vpts.SetData(_vtk.numpy_to_vtk(np.ascontiguousarray(orig), deep=True))
npts = orig.shape[0]
vcells = pyvista.core.cell.CellArray.from_regular_cells(
np.arange(npts, dtype=pyvista.ID_TYPE).reshape((npts, 1)),
)
# Create vtkPolyData object
pdata = _vtk.vtkPolyData()
pdata.SetPoints(vpts)
pdata.SetVerts(vcells)
# Add vectors to polydata
name = 'vectors'
vtkfloat = _vtk.numpy_to_vtk(np.ascontiguousarray(vec), deep=True)
vtkfloat.SetName(name)
pdata.GetPointData().AddArray(vtkfloat)
pdata.GetPointData().SetActiveVectors(name)
# Add magnitude of vectors to polydata
name = 'mag'
scalars = (vec * vec).sum(1) ** 0.5
vtkfloat = _vtk.numpy_to_vtk(np.ascontiguousarray(scalars), deep=True)
vtkfloat.SetName(name)
pdata.GetPointData().AddArray(vtkfloat)
pdata.GetPointData().SetActiveScalars(name)
return pyvista.PolyData(pdata)
@overload
def principal_axes(points: MatrixLike[float]) -> NumpyArray[float]: ...
@overload
def principal_axes(
points: MatrixLike[float],
*,
return_std: Literal[True] = True,
) -> tuple[NumpyArray[float], NumpyArray[float]]: ...
@overload
def principal_axes(
points: MatrixLike[float],
*,
return_std: Literal[False] = False,
) -> NumpyArray[float]: ...
@overload
def principal_axes(
points: MatrixLike[float], *, return_std: bool = ...
) -> NumpyArray[float] | tuple[NumpyArray[float], NumpyArray[float]]: ...
def principal_axes(
points: MatrixLike[float], *, return_std: bool = False
) -> NumpyArray[float] | tuple[NumpyArray[float], NumpyArray[float]]:
"""Compute the principal axes of a set of points.
Principal axes are orthonormal vectors that best fit a set of points. The axes
are also known as the principal components in Principal Component Analysis (PCA),
or the right singular vectors from the Singular Value Decomposition (SVD).
The axes are computed as the eigenvectors of the covariance matrix from the
mean-centered points, and are processed to ensure that they form a right-handed
coordinate frame.
The axes explain the total variance of the points. The first axis explains the
largest percentage of variance, followed by the second axis, followed again by
the third axis which explains the smallest percentage of variance.
The axes may be used to build an oriented bounding box or to align the points to
another set of axes (e.g. the world XYZ axes).
.. note::
The computed axes are not unique, and the sign of each axis direction can be
arbitrarily changed.
.. note::
This implementation creates a temporary array of the same size as the input
array, and is therefore not optimal in terms of its memory requirements.
A more memory-efficient computation may be supported in a future release.
.. versionadded:: 0.45.0
See Also
--------
fit_plane_to_points
Fit a plane to points using the first two principal axes.
pyvista.DataSetFilters.align_xyz
Filter which aligns principal axes to the x-y-z axes.
Parameters
----------
points : MatrixLike[float]
Nx3 array of points.
return_std : bool, default: False
If ``True``, also returns the standard deviation of the points along each axis.
Standard deviation is computed as the square root of the eigenvalues of the
mean-centered covariance matrix.
Returns
-------
numpy.ndarray
3x3 orthonormal array with the principal axes as row vectors.
numpy.ndarray
Three-item array of the standard deviations along each axis.
Examples
--------
>>> import pyvista as pv
>>> import numpy as np
>>> rng = np.random.default_rng(seed=0) # only seeding for the example
Create a mesh with points that have the largest variation in ``X``,
followed by ``Y``, then ``Z``.
>>> radii = np.array((6, 3, 1)) # x-y-z radii
>>> mesh = pv.ParametricEllipsoid(
... xradius=radii[0], yradius=radii[1], zradius=radii[2]
... )
Plot the mesh and highlight its points in black.
>>> p = pv.Plotter()
>>> _ = p.add_mesh(mesh)
>>> _ = p.add_points(mesh, color='black')
>>> _ = p.show_grid()
>>> p.show()
Compute its principal axes and return the standard deviations.
>>> axes, std = pv.principal_axes(mesh.points, return_std=True)
>>> axes
pyvista_ndarray([[-1.0000000e+00, -3.8287229e-08, 3.6589407e-10],
[-3.8287229e-08, 1.0000000e+00, -3.0685656e-09],
[-3.6589393e-10, -3.0685656e-09, -1.0000000e+00]],
dtype=float32)
Note that the principal axes have ones along the diagonal and zeros
in the off-diagonal. This indicates that the first principal axis is
aligned with the x-axis, the second with the y-axis, and third with
the z-axis. This is expected, since the mesh is already axis-aligned.
However, since the signs of the principal axes are arbitrary, the
first and third axes in this case have a negative direction.
Show the standard deviation along each axis.
>>> std
array([3.014956 , 1.507478 , 0.7035637], dtype=float32)
Compare this to using :meth:`numpy.std` for the computation.
>>> np.std(mesh.points, axis=0)
pyvista_ndarray([3.0149572, 1.5074761, 0.7035699], dtype=float32)
Since the points are axis-aligned, the two results agree in this case. In general,
however, these two methods differ in that :meth:`numpy.std` with `axis=0` computes
the standard deviation along the `x-y-z` axes, whereas the standard deviation
returned by :meth:`principal_axes` is computed along the principal axes.
Convert the values to proportions for analysis.
>>> std / sum(std)
array([0.5769149 , 0.28845742, 0.1346276 ], dtype=float32)
From this result, we can determine that the axes explain approximately
58%, 29%, and 13% of the total variance in the points, respectively.
Let's compare this to the proportions of the known radii of the ellipsoid.
>>> radii / sum(radii)
array([0.6, 0.3, 0.1])
Note how the two ratios are similar, but do not match exactly. This is
because the points of the ellipsoid are prolate and are denser near the
poles. If the points were normally distributed, however, the proportions
would match exactly.
Create an array of normally distributed points scaled along the x-y-z axes.
Use the same scaling as the radii of the ellipsoid from the previous example.
>>> normal_points = rng.normal(size=(1000, 3))
>>> scaled_points = normal_points * radii
>>> axes, std = pv.principal_axes(scaled_points, return_std=True)
>>> axes
array([[-0.99997578, 0.00682346, 0.00136972],
[ 0.00681368, 0.99995213, -0.00702282],
[-0.00141757, -0.00701331, -0.9999744 ]])
Once again, the axes have ones along the diagonal as expected since the
points are already axis-aligned. Now let's examine the standard deviation
and compare the relative proportions.
>>> std
array([5.94466738, 2.89590334, 1.02103169])
>>> std / sum(std)
array([0.60280948, 0.29365444, 0.10353608])
>>> radii / sum(radii)
array([0.6, 0.3, 0.1])
Since the points are normally distributed, the relative proportion of
the standard deviation matches the scaling of the axes almost perfectly.
"""
points = _validation.validate_arrayNx3(points)
points_centered = points - np.mean(points, axis=0)
eig_vals, eig_vectors = np.linalg.eigh(points_centered.T @ points_centered)
axes = eig_vectors.T[::-1] # columns, ascending order -> rows, descending order
# Ensure axes form a right-handed coordinate frame
if np.linalg.det(axes) < 0:
axes[2] *= -1
if return_std:
# Compute standard deviation and swap order from ascending -> descending
std = np.sqrt(np.abs(eig_vals) / len(points))[::-1]
return axes, std
return axes
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,312 @@
"""Context manager for controlling global state variables."""
from __future__ import annotations
from abc import ABC
from abc import abstractmethod
import contextlib
from typing import TYPE_CHECKING
from typing import Generic
from typing import Literal
from typing import TypeVar
from typing import cast
from typing import final
from typing import get_args
from typing import overload
from pyvista.core import _vtk_core as _vtk
if TYPE_CHECKING:
from typing_extensions import Self
T = TypeVar('T')
class _StateManager(contextlib.AbstractContextManager[None], ABC, Generic[T]):
"""Abstract base class for managing a global state variable.
Subclasses must:
- Specify a `Literal` as the subclass' type argument. The literal's
arguments must specify all allowable options for the state variable.
- Define a getter and setter for the state. Input validation is not
required - the input is automatically validated when setting the state.
Examples
--------
>>> from pyvista.core.utilities.state_manager import _StateManager
>>> from typing import Literal
Define the available options as a ``Literal`` and initialize a global state variable.
>>> _StateOptions = Literal['on', 'off']
>>> _GLOBAL_STATE = ['off'] # Init global state. Use list to make it mutable.
Define the class and its state property.
>>> class MyState(_StateManager[_StateOptions]):
... @property
... def _state(self) -> _StateOptions:
... return _GLOBAL_STATE[0]
...
... @_state.setter
... def _state(self, state: _StateOptions) -> None:
... _GLOBAL_STATE[0] = state
Finally, create an instance of the state manager.
>>> my_state = MyState()
Get the state.
>>> my_state()
'off'
Set the state.
>>> _ = my_state('on')
>>> my_state()
'on'
Use it as a context manager to set the state temporarily:
>>> with my_state('off'):
... pass
"""
@classmethod
def _get_state_options_from_literal(cls) -> tuple[str | int | bool]:
state_manager_fullname = f'{_StateManager.__module__}.{_StateManager.__name__}'
for base in getattr(cls, '__orig_bases__', ()):
if str(base).startswith(state_manager_fullname):
# Get StateManager's typing args
state_manager_args = get_args(base)
if len(state_manager_args) == 1:
# There must only be one arg and it must be a non-empty Literal
literal = state_manager_args[0]
if str(literal).startswith('typing.Literal'):
args = get_args(literal)
if len(args) >= 1:
return args
msg = (
'Type argument for subclasses must be a single non-empty Literal with all state '
'options provided.'
)
raise TypeError(msg)
def __init__(self) -> None:
"""Initialize context manager."""
self._valid_states = self._get_state_options_from_literal()
self._original_state: T | None = None
@property
@abstractmethod
def _state(self) -> T:
"""Get the current global state."""
@_state.setter
@abstractmethod
def _state(self, state: T) -> None:
"""Set the global state."""
@final
def _validate_state(self, state: T) -> T:
from pyvista import _validation # noqa: PLC0415
_validation.check_contains(self._valid_states, must_contain=state, name='state')
return state
def __enter__(self) -> None:
"""Enter context manager."""
if self._original_state is None:
msg = 'State must be set before using it as a context manager.'
raise ValueError(msg)
def __exit__(self, exc_type, exc_value, traceback): # noqa: ANN001, ANN204
"""Exit context manager and restore original state."""
self._state = cast('T', self._original_state)
self._original_state = None # Reset
@overload
def __call__(self: Self, state: None) -> T: ...
@overload
def __call__(self: Self, state: T) -> Self: ...
def __call__(self: Self, state: T | None = None) -> Self | T:
"""Call the context manager."""
if state is None:
return self._state
self._validate_state(state)
# Create new instance and store the local state to be restored when exiting
output = self.__class__()
output._original_state = self._state
output._state = state
return output
_VerbosityOptions = Literal[
'off',
'error',
'warning',
'info',
'max',
]
class _VTKVerbosity(_StateManager[_VerbosityOptions]):
"""Context manager to set VTK verbosity level.
.. versionadded:: 0.45
Parameters
----------
verbosity : str
Verbosity of the :vtk:`vtkLogger` to set.
- ``'off'``: No output.
- ``'error'``: Only error messages.
- ``'warning'``: Errors and warnings.
- ``'info'``: Errors, warnings, and info messages.
- ``'max'``: All messages, including debug info.
Examples
--------
Get the current vtk verbosity.
>>> import pyvista as pv
>>> pv.vtk_verbosity()
'info'
Set verbosity to max.
>>> _ = pv.vtk_verbosity('max')
>>> pv.vtk_verbosity()
'max'
Create a :func:`~pyvista.Sphere`. Note how many VTK debugging messages are now
generated as the sphere is created.
>>> mesh = pv.Sphere()
Use it as a context manager to temporarily turn it off.
>>> with pv.vtk_verbosity('off'):
... mesh = mesh.cell_quality('volume')
The state is restored to its previous value outside the context.
>>> pv.vtk_verbosity()
'max'
Note that the verbosity state is global and will persist between function
calls. If the context manager isn't used, the state needs to be reset explicitly.
Here, we set it back to its default value.
>>> _ = pv.vtk_verbosity('info')
"""
@property
def _state(self) -> _VerbosityOptions:
int_to_string: dict[int, _VerbosityOptions] = {
-9: 'off',
-2: 'error',
-1: 'warning',
0: 'info',
9: 'max',
}
state = _vtk.vtkLogger.GetCurrentVerbosityCutoff()
try:
return int_to_string[state]
except KeyError:
# Unsupported state, raise error using validation method
self._validate_state(state) # type: ignore[arg-type]
msg = 'This line should not be reachable.' # pragma: no cover
raise RuntimeWarning(msg) # pragma: no cover
@_state.setter
def _state(self, state: _VerbosityOptions) -> None:
verbosity_int = _vtk.vtkLogger.ConvertToVerbosity(state.upper())
_vtk.vtkLogger.SetStderrVerbosity(verbosity_int)
vtk_verbosity = _VTKVerbosity()
_VtkSnakeCaseOptions = Literal['allow', 'warning', 'error']
class _vtkSnakeCase(_StateManager[_VtkSnakeCaseOptions]): # noqa: N801
"""Context manager to control access to VTK's pythonic snake_case API.
VTK 9.4 introduced pythonic snake_case attributes, e.g. `output_port` instead
of `GetOutputPort`. These can easily be confused for PyVista attributes
which also use a snake_case convention. This class controls access to vtk's
new interface.
.. versionadded:: 0.45
Parameters
----------
state : 'allow' | 'warning' | 'error'
Allow or disallow the use of VTK's pythonic snake_case API with
PyVista-wrapped VTK classes.
- 'allow': Allow accessing VTK-defined snake_case attributes.
- 'warning': Print a RuntimeWarning when accessing VTK-defined snake_case
attributes.
- 'error': Raise a ``PyVistaAttributeError`` when accessing
VTK-defined snake_case attributes.
Examples
--------
Get the current access state for VTK's snake_case api.
>>> import pyvista as pv
>>> pv.vtk_snake_case()
'error'
The following will raise an error because the `information` property is defined
by :vtk:`vtkDataObject` and is not part of PyVista's API.
>>> # pv.PolyData().information
Allow use of VTK's snake_case attributes. No warning or error is raised.
>>> _ = pv.vtk_snake_case('allow')
>>> pv.PolyData().information
<vtkmodules.vtkCommonCore.vtkInformation...
Note that this state is global and will persist between function calls. Set it
back to its original state explicitly.
>>> _ = pv.vtk_snake_case('error')
Use it as a context manager instead. This way, the state is only temporarily
modified and is automatically restored.
>>> with pv.vtk_snake_case('allow'):
... _ = pv.PolyData().information
>>> pv.vtk_snake_case()
'error'
"""
@property
def _state(self) -> _VtkSnakeCaseOptions:
import pyvista as pv # noqa: PLC0415
return pv._VTK_SNAKE_CASE_STATE
@_state.setter
def _state(self, state: _VtkSnakeCaseOptions) -> None:
import pyvista as pv # noqa: PLC0415
pv._VTK_SNAKE_CASE_STATE = state
vtk_snake_case = _vtkSnakeCase()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,537 @@
"""Module implementing point transformations and their matrices."""
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import Literal
from typing import overload
import numpy as np
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _validation
if TYPE_CHECKING:
from pyvista.core._typing_core import NumpyArray
from pyvista.core._typing_core import TransformLike
from pyvista.core._typing_core import VectorLike
@_deprecate_positional_args(allowed=['axis', 'angle'])
def axis_angle_rotation( # noqa: PLR0917
axis: VectorLike[float],
angle: float,
point: VectorLike[float] | None = None,
deg: bool = True, # noqa: FBT001, FBT002
) -> NumpyArray[float]:
r"""Return a 4x4 matrix for rotation about any axis by given angle.
Rotations around an axis that contains the origin can easily be
computed using Rodrigues' rotation formula. The key quantity is
the ``K`` cross product matrix for the unit vector ``n`` defining
the axis of the rotation:
/ 0 -nz ny \
K = | nz 0 -nx |
\ -ny nx 0 /
For a rotation angle ``phi`` around the vector ``n`` the rotation
matrix is given by
R = I + sin(phi) K + (1 - cos(phi)) K^2
where ``I`` is the 3-by-3 unit matrix and ``K^2`` denotes the matrix
square of ``K``.
If the rotation axis doesn't contain the origin, we have to first
shift real space to transform the axis' ``p0`` reference point into
the origin, then shift the points back after rotation:
p' = R @ (p - p0) + p0 = R @ p + (p0 - R @ p0)
This means that the rotation in general consists of a 3-by-3
rotation matrix ``R``, and a translation given by
``b = p0 - R @ p0``. These can be encoded in a 4-by-4 transformation
matrix by filling the 3-by-3 leading principal submatrix with ``R``,
and filling the top 3 values in the last column with ``b``.
Parameters
----------
axis : sequence[float]
The direction vector of the rotation axis. It need not be a
unit vector, but it must not be a zero vector.
angle : float
Angle of rotation around the axis. The angle is defined as a
counterclockwise rotation when facing the normal vector of the
rotation axis. Passed either in degrees or radians depending on
the value of ``deg``.
point : sequence[float], optional
The origin of the rotation (a reference point through which the
rotation axis passes). By default the rotation axis contains the
origin.
deg : bool, default: True
Whether the angle is specified in degrees. ``False`` implies
radians.
Returns
-------
numpy.ndarray
The ``(4, 4)`` rotation matrix.
Examples
--------
Generate a transformation matrix for rotation around a cube's body
diagonal by 120 degrees.
>>> import numpy as np
>>> from pyvista import transformations
>>> trans = transformations.axis_angle_rotation([1, 1, 1], 120)
Check that the transformation cycles the cube's three corners.
>>> corners = np.array(
... [
... [1, 0, 0],
... [0, 1, 0],
... [0, 0, 1],
... ]
... )
>>> rotated = transformations.apply_transformation_to_points(
... trans, corners
... )
>>> np.allclose(rotated, corners[[1, 2, 0], :])
True
"""
if deg:
# convert to radians
angle *= np.pi / 180
# return early for no rotation; play it safe and check only exact equality
if angle % (2 * np.pi) == 0:
return np.eye(4)
axis_ = _validation.validate_array3(axis, dtype_out=float, name='axis')
if point is not None:
point_ = _validation.validate_array3(point, dtype_out=float, name='point')
# check and normalize
axis_norm = np.linalg.norm(axis_)
if np.isclose(axis_norm, 0):
msg = 'Cannot rotate around zero vector axis.'
raise ValueError(msg)
if not np.isclose(axis_norm, 1):
axis_ = axis_ / axis_norm
# build Rodrigues' rotation matrix
K = np.zeros((3, 3))
K[[2, 0, 1], [1, 2, 0]] = axis_
K += -K.T
# the cos and sin functions can introduce some numerical error
# round the elements to exact values for special cases where we know
# sin/cos should evaluate exactly to 0 or 1
sin_angle = np.sin(angle)
cos_angle = np.cos(angle)
if angle % (np.pi / 2) == 0:
cos_angle = round(cos_angle)
sin_angle = round(sin_angle)
R = np.eye(3) + sin_angle * K + (1 - cos_angle) * K @ K
augmented = np.eye(4)
augmented[:-1, :-1] = R
if point is not None:
# rotation of point p would be R @ (p - point) + point
# which is R @ p + (point - R @ point)
augmented[:-1, -1] = point_ - R @ point_
return augmented
def reflection(
normal: VectorLike[float], point: VectorLike[float] | None = None
) -> NumpyArray[float]:
"""Return a 4x4 matrix for reflection across a normal about a point.
Projection to a unit vector ``n`` can be computed using the dyadic
product (or outer product) ``P`` of ``n`` with itself, which is a
3-by-3 symmetric matrix.
Reflection across a plane that contains the origin amounts to
reversing the components of real space points that are perpendicular
to the reflection plane. This gives us the transformation ``R``
acting on a point ``p`` as
p' = R @ p = p - 2 P @ p = (I - 2 P) @ p
so the reflection's transformation matrix is the unit matrix minus
twice the dyadic product ``P``.
If additionally we want to compute a reflection to a plane that does
not contain the origin, we can we can first shift every point in
real space by ``-p0`` (if ``p0`` is a point that lies on the plane)
p' = R @ (p - p0) + p0 = R @ p + (p0 - R @ p0)
This means that the reflection in general consists of a 3-by-3
reflection matrix ``R``, and a translation given by
``b = p0 - R @ p0``. These can be encoded in a 4-by-4 transformation
matrix by filling the 3-by-3 leading principal submatrix with ``R``,
and filling the top 3 values in the last column with ``b``.
Parameters
----------
normal : sequence[float]
The normal vector of the reflection plane. It need not be a unit
vector, but it must not be a zero vector.
point : sequence[float], optional
The origin of the reflection (a reference point through which
the reflection plane passes). By default the reflection plane
contains the origin.
Returns
-------
ndarray
A ``(4, 4)`` transformation matrix for reflecting points across the
plane defined by the given normal and point.
Examples
--------
Generate a transformation matrix for reflection over the XZ plane.
>>> import numpy as np
>>> from pyvista import transformations
>>> trans = transformations.reflection([0, 1, 0])
Check that the reflection transforms corners of a cube among one
another.
>>> verts = np.array(
... [
... [1, -1, 1],
... [-1, -1, 1],
... [-1, -1, -1],
... [-1, -1, 1],
... [1, 1, 1],
... [-1, 1, 1],
... [-1, 1, -1],
... [-1, 1, 1],
... ]
... )
>>> mirrored = transformations.apply_transformation_to_points(trans, verts)
>>> np.allclose(mirrored, verts[[np.r_[4:8, 0:4]], :])
True
"""
normal = np.asarray(normal, dtype='float64')
if normal.shape != (3,):
msg = 'Normal must be a 3-length array-like.'
raise ValueError(msg)
if point is not None:
point = np.asarray(point)
if point.shape != (3,):
msg = 'Plane reference point must be a 3-length array-like.'
raise ValueError(msg)
# check and normalize
normal_norm = np.linalg.norm(normal)
if np.isclose(normal_norm, 0):
msg = 'Plane normal cannot be zero.'
raise ValueError(msg)
if not np.isclose(normal_norm, 1):
normal = normal / normal_norm
# build reflection matrix
projection = np.outer(normal, normal)
R = np.eye(3) - 2 * projection
augmented = np.eye(4)
augmented[:-1, :-1] = R
if point is not None:
# reflection of point p would be R @ (p - point) + point
# which is R @ p + (point - R @ point)
augmented[:-1, -1] = point - R @ point
return augmented
@overload
def apply_transformation_to_points(
transformation: NumpyArray[float],
points: NumpyArray[float],
inplace: Literal[True] = True, # noqa: FBT002
) -> None: ...
@overload
def apply_transformation_to_points(
transformation: NumpyArray[float],
points: NumpyArray[float],
inplace: Literal[False] = False, # noqa: FBT002
) -> NumpyArray[float]: ...
@overload
def apply_transformation_to_points(
transformation: NumpyArray[float],
points: NumpyArray[float],
inplace: bool = ..., # noqa: FBT001
) -> NumpyArray[float] | None: ...
@_deprecate_positional_args(allowed=['transformation', 'points'])
def apply_transformation_to_points(
transformation: NumpyArray[float],
points: NumpyArray[float],
inplace: Literal[True, False] = False, # noqa: FBT002
) -> NumpyArray[float] | None:
"""Apply a given transformation matrix (3x3 or 4x4) to a set of points.
Parameters
----------
transformation : np.ndarray
Transformation matrix of shape (3, 3) or (4, 4).
points : np.ndarray
Array of points to be transformed of shape (N, 3).
inplace : bool, default: False
Updates points in-place while returning nothing.
Returns
-------
numpy.ndarray
Transformed points.
Examples
--------
Scale a set of points in-place.
>>> import numpy as np
>>> import pyvista as pv
>>> from pyvista import examples
>>> points = examples.load_airplane().points
>>> points_orig = points.copy()
>>> scale_factor = 2
>>> tf = scale_factor * np.eye(4)
>>> tf[3, 3] = 1
>>> pv.core.utilities.transformations.apply_transformation_to_points(
... tf, points, inplace=True
... )
>>> assert np.all(np.isclose(points, scale_factor * points_orig))
"""
transformation_shape = transformation.shape
if transformation_shape not in ((3, 3), (4, 4)):
msg = '`transformation` must be of shape (3, 3) or (4, 4).'
raise ValueError(msg)
if points.shape[1] != 3:
msg = '`points` must be of shape (N, 3).'
raise ValueError(msg)
if transformation_shape[0] == 4:
# Divide by scale factor when homogeneous
transformation /= transformation[3, 3]
# Add the homogeneous coordinate
# `points_2` is a copy of the data, not a view
points_2 = np.empty((len(points), 4))
points_2[:, :-1] = points
points_2[:, -1] = 1
else:
points_2 = points # type: ignore[assignment]
# Paged matrix multiplication. For arrays with ndim > 2, matmul assumes
# that the matrices to be multiplied lie in the last two dimensions.
points_2 = (transformation[np.newaxis, :, :] @ points_2.T)[0, :3, :].T
# If inplace, set the points
if inplace:
points[:] = points_2
return None
else:
# otherwise return the new points
return points_2
def decomposition(
transformation: TransformLike,
*,
homogeneous: bool = False,
) -> tuple[
NumpyArray[float], NumpyArray[float], NumpyArray[float], NumpyArray[float], NumpyArray[float]
]:
"""Decompose a transformation into its components.
The transformation matrix ``M`` is decomposed into five components:
- translation ``T``
- rotation ``R``
- reflection ``N``
- scaling ``S``
- shearing ``K``
such that, when represented as 4x4 matrices, ``M = TRNSK``. The decomposition is
unique and is computed with polar matrix decomposition.
By default, compact representations of the transformations are returned (e.g. as a
3-element vector or a 3x3 matrix). Optionally, 4x4 matrices may be returned instead.
.. note::
- The rotation is orthonormal and right-handed with positive determinant.
- The scaling factors are positive.
- The reflection is either ``1`` (no reflection) or ``-1`` (has reflection)
and can be used like a scaling factor.
Parameters
----------
transformation : TransformLike
Array or transform to decompose.
homogeneous : bool, default: False
If ``True``, return the components (translation, rotation, etc.) as 4x4
homogeneous matrices. By default, reflection is a scalar, translation and
scaling are length-3 vectors, and rotation and shear are 3x3 matrices.
Returns
-------
numpy.ndarray
Translation component ``T``. Returned as a 3-element vector (or a 4x4
translation matrix if ``homogeneous`` is ``True``).
numpy.ndarray
Rotation component ``R``. Returned as a 3x3 orthonormal rotation matrix of row
vectors (or a 4x4 rotation matrix if ``homogeneous`` is ``True``).
numpy.ndarray
Reflection component ``N``. Returned as a NumPy scalar (or a 4x4 reflection
matrix if ``homogeneous`` is ``True``).
numpy.ndarray
Scaling component ``S``. Returned as a 3-element vector (or a 4x4 scaling matrix
if ``homogeneous`` is ``True``).
numpy.ndarray
Shear component ``K``. Returned as a 3x3 matrix with ones on the diagonal and
shear values in the off-diagonals (or as a 4x4 shearing matrix if ``homogeneous``
is ``True``).
Examples
--------
Decompose a transformation matrix which has scaling, rotation, and translation.
>>> import pyvista as pv
>>> matrix = [
... [0.0, -2.0, 0.0, 4.0],
... [1.0, 0.0, 0.0, 5.0],
... [0.0, 0.0, 3.0, 6.0],
... [0.0, 0.0, 0.0, 1.0],
... ]
>>> T, R, N, S, K = pv.transformations.decomposition(matrix)
Since the input has no shear, this component is the identity matrix.
>>> K # shear
array([[1., 0., 0.],
[0., 1., 0.],
[0., 0., 1.]])
>>> S # scale
array([1., 2., 3.])
There is no reflection so this component is ``1``.
>>> N # reflection
array(1.)
>>> R # rotation
array([[ 0., -1., 0.],
[ 1., 0., 0.],
[ 0., 0., 1.]])
>>> T # translation
array([4., 5., 6.])
Repeat the example, but this time with a small shear component of 0.1. Note how the
presence of shear also affects the values of the scaling and rotation components.
>>> matrix = [
... [0.0, -2.0, 0.0, 4.0],
... [1.0, 0.1, 0.0, 5.0],
... [0.0, 0.0, 3.0, 6.0],
... [0.0, 0.0, 0.0, 1.0],
... ]
>>> T, R, N, S, K = pv.transformations.decomposition(matrix)
>>> K # shear
array([[1. , 0.03333333, 0. ],
[0.01663894, 1. , 0. ],
[0. , 0. , 1. ]])
>>> S # scale
array([0.99944491, 2.0022213 , 3. ])
>>> N # reflection
array(1.)
>>> R # rotation
array([[ 0.03331483, -0.99944491, 0. ],
[ 0.99944491, 0.03331483, 0. ],
[ 0. , 0. , 1. ]])
>>> T # translation
array([4., 5., 6.])
"""
matrix4x4 = _validation.validate_transform4x4(transformation)
dtype_out = matrix4x4.dtype
I3 = np.eye(3, dtype=dtype_out)
I4 = np.eye(4, dtype=dtype_out)
matrix3x3 = matrix4x4[:3, :3]
T = matrix4x4[:3, 3]
RN, SK = _polar_decomposition(matrix3x3)
# Get scale from diagonals and shear from off-diagonals
S = np.diagonal(SK).copy() # Copy since it's read only
K = (SK * (I3 == 0.0)) / S[:, np.newaxis] + I3
# Get reflection and ensure rotation is right-handed
if np.linalg.det(RN) < 0:
# Reflections are present
R = RN * -1
N = np.array(-1, dtype=dtype_out)
else:
R = RN
N = np.array(1, dtype=dtype_out)
if homogeneous:
T4 = I4.copy()
T4[:3, 3] = T
R4 = I4.copy()
R4[:3, :3] = R
N4 = I4.copy()
N4[:3, :3] = I3 * N
S4 = I4.copy()
S4[:3, :3] = I3 * S
K4 = I4.copy()
K4[:3, :3] = K
return T4, R4, N4, S4, K4
return T, R, N, S, K
def _polar_decomposition(a: NumpyArray[float]) -> tuple[NumpyArray[float], NumpyArray[float]]:
# Decompose `a=up` where u is orthonormal and p is positive semi-definite
# See scipy.linalg.polar for details
w, s, vh = np.linalg.svd(a, full_matrices=False)
u = w.dot(vh)
p = (vh.T.conj() * s).dot(vh)
return u, p
@@ -0,0 +1,69 @@
"""Wrapper mapping.
Setting ``pyvista._wrappers`` allows for developers to override the default class used
to coerce a :vtk:`vtkDataSet` into a pyvista object. This is useful when creating a
subclass of a :class:`pyvista.DataSet` class.
Examples
--------
A user-defined Foo class is defined that extends the functionality of
:class:`pyvista.PolyData`. This class is set as the default wrapper for
:vtk:`vtkPolyData` objects.
>>> import pyvista as pv
>>> default_wrappers = pv._wrappers.copy()
>>> class Foo(pv.PolyData):
... pass # Extend PolyData here
>>> pv._wrappers['vtkPolyData'] = Foo
>>> image = pv.ImageData()
>>> surface = image.extract_surface()
>>> assert isinstance(surface, Foo)
>>> pv._wrappers = default_wrappers # reset back to default
"""
from __future__ import annotations
from typing import TypeVar
from . import _vtk_core as _vtk
from .composite import MultiBlock
from .grid import ImageData
from .grid import RectilinearGrid
from .objects import Table
from .partitioned import PartitionedDataSet
from .pointset import ExplicitStructuredGrid
from .pointset import PointSet
from .pointset import PolyData
from .pointset import StructuredGrid
from .pointset import UnstructuredGrid
_wrappers = {
'vtkExplicitStructuredGrid': ExplicitStructuredGrid,
'vtkUnstructuredGrid': UnstructuredGrid,
'vtkRectilinearGrid': RectilinearGrid,
'vtkStructuredGrid': StructuredGrid,
'vtkPolyData': PolyData,
'vtkImageData': ImageData,
'vtkStructuredPoints': ImageData,
'vtkMultiBlockDataSet': MultiBlock,
'vtkTable': Table,
'vtkPointSet': PointSet,
'vtkPartitionedDataSet': PartitionedDataSet,
# 'vtkParametricSpline': pyvista.Spline,
}
_WrappableVTKDataObjectType = TypeVar( # noqa: PYI018
'_WrappableVTKDataObjectType',
_vtk.vtkExplicitStructuredGrid,
_vtk.vtkUnstructuredGrid,
_vtk.vtkRectilinearGrid,
_vtk.vtkStructuredGrid,
_vtk.vtkPolyData,
_vtk.vtkImageData,
_vtk.vtkStructuredPoints,
_vtk.vtkMultiBlockDataSet,
_vtk.vtkTable,
_vtk.vtkPoints,
_vtk.vtkPartitionedDataSet,
)
@@ -0,0 +1,17 @@
"""PyVista Demos."""
from __future__ import annotations
from pyvista.demos.demos import glyphs as glyphs
from pyvista.demos.demos import orientation_cube as orientation_cube
from pyvista.demos.demos import orientation_plotter as orientation_plotter
from pyvista.demos.demos import plot_ants_plane as plot_ants_plane
from pyvista.demos.demos import plot_beam as plot_beam
from pyvista.demos.demos import plot_datasets as plot_datasets
from pyvista.demos.demos import plot_glyphs as plot_glyphs
from pyvista.demos.demos import plot_wave as plot_wave
from pyvista.demos.logo import logo_atomized as logo_atomized
from pyvista.demos.logo import logo_basic as logo_basic
from pyvista.demos.logo import logo_letters as logo_letters
from pyvista.demos.logo import logo_voxel as logo_voxel
from pyvista.demos.logo import plot_logo as plot_logo
@@ -0,0 +1,575 @@
"""Demos to show off the functionality of PyVista."""
from __future__ import annotations
import time
import numpy as np
import pyvista
from pyvista import examples
from pyvista._deprecate_positional_args import _deprecate_positional_args
from .logo import text_3d
def glyphs(grid_sz=3):
"""Create several parametric supertoroids using VTK's glyph table functionality.
Parameters
----------
grid_sz : int, default: 3
Create ``grid_sz x grid_sz`` supertoroids.
Returns
-------
pyvista.PolyData
Mesh of supertoroids.
See Also
--------
plot_glyphs
Examples
--------
>>> from pyvista import demos
>>> mesh = demos.glyphs()
>>> mesh.plot()
"""
# Seed rng for reproducible plots
rng = np.random.default_rng(seed=0)
n = 10
values = np.arange(n) # values for scalars to look up glyphs by
# taken from:
params = rng.uniform(0.5, 2, size=(n, 2)) # (n1, n2) parameters for the toroids
geoms = [pyvista.ParametricSuperToroid(n1=n1, n2=n2) for n1, n2 in params]
# get dataset where to put glyphs
grid_sz = float(grid_sz)
x, y, z = np.mgrid[:grid_sz, :grid_sz, :grid_sz]
mesh = pyvista.StructuredGrid(x, y, z)
# add random scalars
rng_int = rng.integers(0, n, size=x.size)
mesh.point_data['scalars'] = rng_int
# construct the glyphs on top of the mesh; don't scale by scalars now
return mesh.glyph(
geom=geoms,
indices=values,
scale=False,
factor=0.3,
rng=(0, n - 1),
orient=False,
)
def plot_glyphs(grid_sz=3, **kwargs):
"""Plot several parametric supertoroids using VTK's glyph table functionality.
Parameters
----------
grid_sz : int, default: 3
Create ``grid_sz x grid_sz`` supertoroids.
**kwargs : dict, optional
All additional keyword arguments will be passed to
:func:`pyvista.Plotter.add_mesh`.
Returns
-------
list | np.ndarray | ipywidgets.Widget
See :func:`show <pyvista.Plotter.show>`.
Examples
--------
>>> from pyvista import demos
>>> demos.plot_glyphs()
"""
# construct the glyphs on top of the mesh; don't scale by scalars now
mesh = glyphs(grid_sz)
kwargs.setdefault('specular', 1)
kwargs.setdefault('specular_power', 15)
kwargs.setdefault('smooth_shading', True)
# create plotter and add our glyphs with some nontrivial lighting
plotter = pyvista.Plotter()
plotter.add_mesh(mesh, show_scalar_bar=False, **kwargs)
return plotter.show()
def orientation_cube():
"""Return a dictionary containing the meshes composing an orientation cube.
Returns
-------
dict
Dictionary containing the meshes composing an orientation cube.
Examples
--------
Load the cube mesh and plot it
>>> import pyvista as pv
>>> from pyvista import demos
>>> ocube = demos.orientation_cube()
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(ocube['cube'], show_edges=True)
>>> _ = pl.add_mesh(ocube['x_p'], color='blue')
>>> _ = pl.add_mesh(ocube['x_n'], color='blue')
>>> _ = pl.add_mesh(ocube['y_p'], color='green')
>>> _ = pl.add_mesh(ocube['y_n'], color='green')
>>> _ = pl.add_mesh(ocube['z_p'], color='red')
>>> _ = pl.add_mesh(ocube['z_n'], color='red')
>>> pl.show_axes()
>>> pl.show()
"""
cube = pyvista.Cube()
x_p = text_3d('X+', depth=0.2)
x_p.points *= 0.45
x_p.rotate_y(90, inplace=True)
x_p.rotate_x(90, inplace=True)
x_p.translate(-np.array(x_p.center), inplace=True)
x_p.translate([0.5, 0, 0], inplace=True)
# x_p.point_data['mesh'] = 1
x_n = text_3d('X-', depth=0.2)
x_n.points *= 0.45
x_n.rotate_y(90, inplace=True)
x_n.rotate_x(90, inplace=True)
x_n.rotate_z(180, inplace=True)
x_n.translate(-np.array(x_n.center), inplace=True)
x_n.translate([-0.5, 0, 0], inplace=True)
# x_n.point_data['mesh'] = 2
y_p = text_3d('Y+', depth=0.2)
y_p.points *= 0.45
y_p.rotate_x(90, inplace=True)
y_p.rotate_z(180, inplace=True)
y_p.translate(-np.array(y_p.center), inplace=True)
y_p.translate([0, 0.5, 0], inplace=True)
# y_p.point_data['mesh'] = 3
y_n = text_3d('Y-', depth=0.2)
y_n.points *= 0.45
y_n.rotate_x(90, inplace=True)
y_n.translate(-np.array(y_n.center), inplace=True)
y_n.translate([0, -0.5, 0], inplace=True)
# y_n.point_data['mesh'] = 4
z_p = text_3d('Z+', depth=0.2)
z_p.points *= 0.45
z_p.rotate_z(90, inplace=True)
z_p.translate(-np.array(z_p.center), inplace=True)
z_p.translate([0, 0, 0.5], inplace=True)
# z_p.point_data['mesh'] = 5
z_n = text_3d('Z-', depth=0.2)
z_n.points *= 0.45
z_n.rotate_x(180, inplace=True)
z_n.translate(-np.array(z_n.center), inplace=True)
z_n.translate([0, 0, -0.5], inplace=True)
return {
'cube': cube,
'x_p': x_p,
'x_n': x_n,
'y_p': y_p,
'y_n': y_n,
'z_p': z_p,
'z_n': z_n,
}
def orientation_plotter():
"""Return a plotter containing the orientation cube.
Returns
-------
pyvista.Plotter
Orientation cube plotter.
Examples
--------
>>> from pyvista import demos
>>> plotter = demos.orientation_plotter()
>>> plotter.show()
"""
ocube = orientation_cube()
pl = pyvista.Plotter()
pl.add_mesh(ocube['cube'], show_edges=True)
pl.add_mesh(ocube['x_p'], color='blue')
pl.add_mesh(ocube['x_n'], color='blue')
pl.add_mesh(ocube['y_p'], color='green')
pl.add_mesh(ocube['y_n'], color='green')
pl.add_mesh(ocube['z_p'], color='red')
pl.add_mesh(ocube['z_n'], color='red')
pl.show_axes() # type: ignore[call-arg]
return pl
@_deprecate_positional_args
def plot_wave(fps=30, frequency=1, wavetime=3, notebook=None): # noqa: PLR0917
"""Plot a 3D moving wave in a render window.
Parameters
----------
fps : int, default: 30
Maximum frames per second to display.
frequency : float, default: 1.0
Wave cycles per second (Hz).
wavetime : float, default: 3.0
The desired total display time in seconds.
notebook : bool, optional
When ``True``, the resulting plot is placed inline a jupyter
notebook. Assumes a jupyter console is active.
Returns
-------
numpy.ndarray
Position of points at last frame.
Examples
--------
>>> from pyvista import demos
>>> out = demos.plot_wave()
"""
# camera position
cpos = [
(6.879481857604187, -32.143727535933195, 23.05622921691103),
(-0.2336056403734026, -0.6960083534590372, -0.7226721553894022),
(-0.008900669873416645, 0.6018246347860926, 0.7985786667826725),
]
# Make data
X = np.arange(-10, 10, 0.25)
Y = np.arange(-10, 10, 0.25)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)
# Create and plot structured grid
sgrid = pyvista.StructuredGrid(X, Y, Z)
mesh = sgrid.extract_surface()
mesh['Height'] = Z.ravel()
# Start a plotter object and set the scalars to the Z height
plotter = pyvista.Plotter(notebook=notebook)
plotter.add_mesh(mesh, scalars='Height', show_scalar_bar=False, smooth_shading=True)
plotter.camera_position = cpos
plotter.show(
title='Wave Example',
window_size=[800, 600],
auto_close=False,
interactive_update=True,
)
# Update Z and display a frame for each updated position
tdelay = 1.0 / fps
tlast = time.time()
tstart = time.time()
while time.time() - tstart < wavetime:
# get phase from start
telap = time.time() - tstart
phase = telap * 2 * np.pi * frequency
Z = np.sin(R + phase)
mesh.points[:, -1] = Z.ravel()
mesh['Height'] = Z.ravel()
mesh.compute_normals(inplace=True)
# Render and get time to render
plotter.update()
# time delay
tpast = time.time() - tlast
if tpast < tdelay and tpast >= 0 and not plotter.off_screen:
time.sleep(tdelay - tpast)
# store when rendering complete
tlast = time.time()
# Close movie and delete object
plotter.close()
return mesh.points
def plot_ants_plane(notebook=None):
"""Plot two ants and airplane.
Demonstrate how to create a plot class to plot multiple meshes while
adding scalars and text.
This example plots the following:
.. code-block:: python
>>> import pyvista as pv
>>> from pyvista import examples
Load and shrink airplane
>>> airplane = examples.load_airplane()
>>> airplane.points /= 10
Rotate and translate ant so it is on the plane.
>>> ant = examples.load_ant()
>>> _ = ant.rotate_x(90, inplace=True)
>>> _ = ant.translate([90, 60, 15], inplace=True)
Make a copy and add another ant.
>>> ant_copy = ant.translate([30, 0, -10], inplace=False)
Create plotting object.
>>> plotter = pv.Plotter()
>>> _ = plotter.add_mesh(ant, color='r')
>>> _ = plotter.add_mesh(ant_copy, color='b')
Add airplane mesh and make the color equal to the Y position.
>>> plane_scalars = airplane.points[:, 1]
>>> _ = plotter.add_mesh(
... airplane,
... scalars=plane_scalars,
... scalar_bar_args={'title': 'Plane Y Location'},
... )
>>> _ = plotter.add_text('Ants and Plane Example')
>>> plotter.show()
Parameters
----------
notebook : bool, optional
When ``True``, the resulting plot is placed inline a jupyter
notebook. Assumes a jupyter console is active.
Examples
--------
>>> from pyvista import demos
>>> demos.plot_ants_plane()
"""
# load and shrink airplane
airplane = examples.load_airplane()
airplane.points /= 10
# rotate and translate ant so it is on the plane
ant = examples.load_ant()
ant.rotate_x(90, inplace=True)
ant.translate([90, 60, 15], inplace=True)
# Make a copy and add another ant
ant_copy = ant.copy()
ant_copy.translate([30, 0, -10], inplace=True)
# Create plotting object
plotter = pyvista.Plotter(notebook=notebook)
plotter.add_mesh(ant, color='r')
plotter.add_mesh(ant_copy, color='b')
# Add airplane mesh and make the color equal to the Y position
plane_scalars = airplane.points[:, 1]
plotter.add_mesh(
airplane,
scalars=plane_scalars,
scalar_bar_args={'title': 'Plane Y\nLocation'},
)
plotter.add_text('Ants and Plane Example')
plotter.show()
def plot_beam(notebook=None):
"""Plot a beam with displacement.
Parameters
----------
notebook : bool, optional
When ``True``, the resulting plot is placed inline a jupyter
notebook. Assumes a jupyter console is active.
Examples
--------
>>> from pyvista import demos
>>> demos.plot_beam()
"""
# Create fiticious displacements as a function of Z location
grid = examples.load_hexbeam()
d = grid.points[:, 2] ** 3 / 250
grid.points[:, 1] += d
# Camera position
cpos = [
(11.915126303095157, 6.11392754955802, 3.6124956735471914),
(0.0, 0.375, 2.0),
(-0.42546442225230097, 0.9024244135964158, -0.06789847673314177),
]
cmap = 'bwr'
# plot this displaced beam
plotter = pyvista.Plotter(notebook=notebook)
plotter.add_mesh(
grid,
scalars=d,
scalar_bar_args={'title': 'Y Displacement'},
rng=[-d.max(), d.max()],
cmap=cmap, # type: ignore[arg-type]
)
plotter.camera_position = cpos
plotter.add_text('Static Beam Example')
plotter.show()
def plot_datasets(dataset_type=None):
"""Plot the pyvista dataset types.
This demo plots the following PyVista dataset types:
* :class:`pyvista.PolyData`
* :class:`pyvista.UnstructuredGrid`
* :class:`pyvista.ImageData`
* :class:`pyvista.RectilinearGrid`
* :class:`pyvista.StructuredGrid`
Parameters
----------
dataset_type : str, optional
If set, plot just that dataset. Must be one of the following:
* ``'PolyData'``
* ``'UnstructuredGrid'``
* ``'ImageData'``
* ``'RectilinearGrid'``
* ``'StructuredGrid'``
Examples
--------
>>> from pyvista import demos
>>> demos.plot_datasets()
"""
allowable_types = [
'PolyData',
'UnstructuredGrid',
'ImageData',
'RectilinearGrid',
'StructuredGrid',
]
if dataset_type is not None and dataset_type not in allowable_types:
msg = (
f'Invalid dataset_type {dataset_type}. '
f'Must be one of the following: {allowable_types}'
)
raise ValueError(msg)
###########################################################################
# uniform grid
image = pyvista.ImageData(dimensions=(6, 6, 1))
image.spacing = (3, 2, 1)
###########################################################################
# RectilinearGrid
xrng = np.array([0, 0.3, 1, 4, 5, 6, 6.2, 6.6])
yrng = np.linspace(-2, 2, 5)
zrng = [1]
rec_grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
###########################################################################
# structured grid
ang = np.linspace(0, np.pi / 2, 10)
r = np.linspace(6, 10, 8)
z = [0]
ang, r, z = np.meshgrid(ang, r, z) # type: ignore[assignment]
x = r * np.sin(ang)
y = r * np.cos(ang)
struct_grid = pyvista.StructuredGrid(x[::-1], y[::-1], z[::-1])
###########################################################################
# polydata
points = pyvista.PolyData([[1.0, 2.0, 2.0], [2.0, 2.0, 2.0]])
line = pyvista.Line()
line.points += np.array((2, 0, 0))
line.clear_data()
tri = pyvista.Triangle()
tri.points += np.array([0, 1, 0])
circ = pyvista.Circle()
circ.points += np.array([1.5, 1.5, 0])
poly = tri + circ
###########################################################################
# unstructuredgrid
pyr = pyvista.Pyramid()
pyr.points *= 0.7
cube = pyvista.Cube(center=(2, 0, 0))
ugrid = circ + pyr + cube + tri
pl = pyvista.Plotter() if dataset_type is not None else pyvista.Plotter(shape='3/2')
# polydata
if dataset_type is None:
pl.subplot(0)
pl.add_text('4. PolyData')
if dataset_type in [None, 'PolyData']:
pl.add_points(points, point_size=20)
pl.add_mesh(line, line_width=5)
pl.add_mesh(poly)
pl.add_mesh(poly.extract_all_edges(), line_width=2, color='k')
# unstructuredgrid
if dataset_type is None:
pl.subplot(1)
pl.add_text('5. UnstructuredGrid')
if dataset_type in [None, 'UnstructuredGrid']:
pl.add_mesh(ugrid)
pl.add_mesh(ugrid.extract_all_edges(), line_width=2, color='k')
# ImageData
if dataset_type is None:
pl.subplot(2)
pl.add_text('1. ImageData')
if dataset_type in [None, 'ImageData']:
pl.add_mesh(image)
pl.add_mesh(image.extract_all_edges(), color='k', style='wireframe', line_width=2)
pl.camera_position = 'xy'
# RectilinearGrid
if dataset_type is None:
pl.subplot(3)
pl.add_text('2. RectilinearGrid')
if dataset_type in [None, 'RectilinearGrid']:
pl.add_mesh(rec_grid)
pl.add_mesh(rec_grid.extract_all_edges(), color='k', style='wireframe', line_width=2)
pl.camera_position = 'xy'
# StructuredGrid
if dataset_type is None:
pl.subplot(4)
pl.add_text('3. StructuredGrid')
if dataset_type in [None, 'StructuredGrid']:
pl.add_mesh(struct_grid)
pl.add_mesh(struct_grid.extract_all_edges(), color='k', style='wireframe', line_width=2)
pl.camera_position = 'xy'
pl.show()
@@ -0,0 +1,362 @@
"""Generate the pyvista logo.
Logos generated with:
plot_logo(screenshot='pyvista_logo.png', window_size=(1920, 1080))
plot_logo(screenshot='pyvista_logo_sm.png', window_size=(960, 400), off_screen=True)
# different camera angle for square plot
cpos = [(-0.3654543687422538, 1.1098808905156292, 9.073223697728247),
(2.553950615449191, 0.34145688392081264, 0.06127122762851659),
(0.019308531920309947, 0.996708840795678, -0.07873161547192065)]
plot_logo(screenshot='pyvista_logo_sm_sq.png', window_size=(960, 960), cpos=cpos,
off_screen=True)
"""
from __future__ import annotations
import os
from pathlib import Path
import numpy as np
import pyvista
from pyvista import examples
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core import _vtk_core as _vtk
from pyvista.core.utilities.features import _voxelize_legacy
THIS_PATH = str(Path(os.path.realpath(__file__)).parent)
LOGO_TITLE = 'PyVista'
def atomize(grid, shift_fac=0.1, scale=0.9):
"""Break apart and shrink and/or scale the individual cells of a mesh.
Parameters
----------
grid : pyvista.UnstructuredGrid
The input mesh to atomize.
shift_fac : float, default: 0.1
Factor by which to shift the individual cells apart.
scale : float, default: 0.9
Factor by which to scale the individual cells.
Returns
-------
pyvista.UnstructuredGrid
The atomized mesh with individually shifted and scaled cells.
"""
cent = grid.center
cells = []
for i in range(grid.n_cells):
cell = grid.extract_cells(i)
ccent = np.array(cell.center)
cell.points[:] = (cell.points - ccent) * scale + ccent
cell.points += (ccent - np.array(cent)) * shift_fac
cells.append(cell)
return cells[0].merge(cells[1:])
def text_3d(string, depth=0.5):
"""Create 3D text from a given string.
Parameters
----------
string : str
The string of text to convert into 3D text.
depth : float, default: 0.5
The depth of the extrusion used to create the 3D text.
Returns
-------
pyvista.DataSet
The 3D text in the form of a PyVista DataSet.
See Also
--------
:ref:`create_pixel_art_example`
"""
from vtkmodules.vtkRenderingFreeType import vtkVectorText # noqa: PLC0415
vec_text = vtkVectorText()
vec_text.SetText(string)
extrude = _vtk.vtkLinearExtrusionFilter()
extrude.SetInputConnection(vec_text.GetOutputPort())
extrude.SetExtrusionTypeToNormalExtrusion()
extrude.SetVector(0, 0, 1)
extrude.SetScaleFactor(depth)
tri_filter = _vtk.vtkTriangleFilter()
tri_filter.SetInputConnection(extrude.GetOutputPort())
tri_filter.Update()
return pyvista.wrap(tri_filter.GetOutput())
@_deprecate_positional_args
def logo_letters(merge=False, depth=0.3): # noqa: FBT002
"""Generate a mesh for each letter in "PyVista".
Parameters
----------
merge : bool, optional
If ``True``, merge the meshes of the individual letters into a single
mesh. If ``False``, return a dictionary where the keys are the letters
and the values are the respective meshes.
depth : float, optional
The depth of the extrusion for each letter in the mesh.
Returns
-------
pyvista.PolyData or dict[str, pyvista.PolyData]
If merge is ``True``, returns a single merged mesh containing all the
letters in "PyVista". If merge is ``False``, returns a dictionary where
the keys are the letters and the values are the respective meshes.
"""
mesh_letters = pyvista.PolyData() if merge else {} # type: ignore[var-annotated]
# spacing between letters
space_factor = 0.9
width = 0
for letter in LOGO_TITLE:
mesh_letter = text_3d(letter, depth=depth)
this_letter_width = mesh_letter.points[:, 0].max()
mesh_letter.translate([width * space_factor, 0, 0.0], inplace=True)
width += this_letter_width
if merge:
mesh_letters += mesh_letter
else:
mesh_letters[letter] = mesh_letter
return mesh_letters
def logo_voxel(density=0.03):
"""Create a voxelized PyVista logo.
Parameters
----------
density : float, default: 0.03
Density of the voxelization.
Returns
-------
pyvista.UnstructuredGrid
Voxelized PyVista logo as an unstructured grid.
"""
return _voxelize_legacy(text_3d(LOGO_TITLE, depth=0.3), density=density)
def logo_basic():
"""Create a basic pyvista logo.
Returns
-------
pyvista.UnstructuredGrid
Grid containing the pyvista letters.
Examples
--------
Plot the basic pyvista logo.
>>> from pyvista import demos
>>> logo = demos.logo_basic()
>>> cpos = logo.plot(smooth_shading=True)
Add scalars and plot the logo.
>>> logo['x_coord'] = logo.points[:, 0]
>>> cpos = logo.plot(
... scalars='x_coord',
... cmap='Spectral',
... smooth_shading=True,
... cpos='xy',
... )
"""
return logo_letters(merge=True).compute_normals(split_vertices=True)
@_deprecate_positional_args
def plot_logo( # noqa: PLR0917
window_size=None,
off_screen=None,
screenshot=None,
cpos=None,
just_return_plotter=False, # noqa: FBT002
show_note=False, # noqa: FBT002
**kwargs,
):
"""Plot the stylized PyVista logo.
Parameters
----------
window_size : sequence[int], optional
Size of the window in the format ``[width, height]``.
off_screen : bool, optional
Renders off screen when ``True``.
screenshot : str, optional
Save screenshot to path when specified.
cpos : list or str, optional
Camera position to use.
just_return_plotter : bool, default: False
Return the plotter instance without rendering.
show_note : bool, default: False
Show a text in the plot when ``True``.
**kwargs : dict, optional
Additional keyword arguments.
Returns
-------
Plotter or camera position
Returns the plotter instance if ``just_return_plotter`` is ``True``,
otherwise returns the camera position if ``screenshot`` is specified,
otherwise shows the plot.
Examples
--------
>>> from pyvista import demos
>>> cpos = demos.plot_logo()
"""
# initialize plotter
if window_size is None:
window_size = [960, 400]
plotter = pyvista.Plotter(window_size=window_size, off_screen=off_screen)
mesh_letters = logo_letters()
# letter 'P'
p_mesh = mesh_letters['P'].compute_normals(split_vertices=True)
plotter.add_mesh(p_mesh, color='#376fa0', smooth_shading=True)
# letter 'y'
y_mesh = mesh_letters['y'].compute_normals(split_vertices=True)
plotter.add_mesh(y_mesh, color='#ffd040', smooth_shading=True)
# letter 'V'
v_grid = _voxelize_legacy(mesh_letters['V'], density=0.08)
v_grid_atom = atomize(v_grid)
v_grid_atom['scalars'] = v_grid_atom.points[:, 0]
v_grid_atom_surf = v_grid_atom.extract_surface()
faces = v_grid_atom_surf.faces.reshape(-1, 5).copy()
faces[:, 1:] = faces[:, 1:][:, ::-1]
v_grid_atom_surf.faces = faces
plotter.add_mesh(
v_grid_atom_surf,
scalars='scalars',
show_edges=True,
cmap='winter',
show_scalar_bar=False,
)
# letter 'i'
i_grid = _voxelize_legacy(mesh_letters['i'], density=0.1)
plotter.add_mesh(
i_grid.extract_surface(),
style='points',
color='r',
render_points_as_spheres=True,
point_size=14,
)
plotter.add_mesh(i_grid, style='wireframe', color='k', line_width=4)
# letter 's'
mesh = mesh_letters['s']
mesh['scalars'] = mesh.points[:, 0]
plotter.add_mesh(
mesh,
scalars='scalars',
style='wireframe',
line_width=2,
cmap='gist_heat',
backface_culling=True,
render_lines_as_tubes=True,
show_scalar_bar=False,
)
# letter 't'
mesh = mesh_letters['t'].clean().compute_normals()
scalars = mesh.points[:, 0]
plotter.add_mesh(mesh, scalars=scalars, show_edges=True, cmap='autumn', show_scalar_bar=False)
# letter 'a'
grid = examples.download_letter_a()
grid.points[:, 0] += mesh_letters['a'].center[0] - grid.center[0]
# select some cells from grid
cells = grid.cells.reshape(-1, 5)
mask = grid.points[cells[:, 1:], 2] < 0.2
mask = mask.all(1)
a_part = grid.extract_cells(mask)
cells = a_part.cells.reshape(-1, 5)
scalars = grid.points[cells[:, 1], 1]
plotter.add_mesh(
a_part, scalars=scalars, show_edges=True, cmap='Greens', show_scalar_bar=False
)
if show_note:
text = text_3d('You can move me!', depth=0.1)
text.points *= 0.1
text.translate([4.0, -0.3, 0], inplace=True)
plotter.add_mesh(text, color='black')
# finalize plot and show it
plotter.set_background(kwargs.pop('background', 'white'))
plotter.camera_position = 'xy'
if 'zoom' in kwargs:
plotter.camera.zoom(kwargs.pop('zoom'))
# plotter.remove_scalar_bar()
plotter.enable_anti_aliasing()
if just_return_plotter:
return plotter
if screenshot: # pragma: no cover
plotter.show(cpos=cpos, auto_close=False)
plotter.screenshot(screenshot, True)
cpos_final = plotter.camera_position
plotter.close()
return cpos_final
else:
return plotter.show(cpos=cpos, **kwargs)
def logo_atomized(density=0.05, scale=0.6, depth=0.05):
"""Generate a voxelized pyvista logo with intra-cell spacing.
Parameters
----------
density : float, default: 0.05
The spacing between voxels in the generated PyVista logo.
scale : float, default: 0.6
The scaling factor for the generated PyVista logo.
depth : float, default: 0.05
The depth of the generated PyVista logo.
Returns
-------
pyvista.UnstructuredGrid
A merged UnstructuredGrid representing the voxelized PyVista logo.
"""
mesh_letters = logo_letters(depth=depth)
grids = []
for letter in mesh_letters.values():
grid = _voxelize_legacy(letter, density=density)
grids.append(atomize(grid, scale=scale))
return grids[0].merge(grids[1:])
@@ -0,0 +1,72 @@
"""Deprecated utilities subpackage."""
from __future__ import annotations
import importlib
import inspect
import warnings
# Places to look for the utility
_MODULES = [
'pyvista.core.errors',
'pyvista.plotting.errors',
]
def _try_import(module, name):
"""Attempt to import a module."""
_module = importlib.import_module(module)
try:
feature = inspect.getattr_static(_module, name)
import_path = f'from {module} import {name}'
except AttributeError:
return None, None
return feature, import_path
def __getattr__(name):
"""Fetch an attribute ``name`` from ``globals()`` and warn if it's from a deprecated module.
Note that ``__getattr__()`` only gets called when ``name`` is missing
from the module's globals. The trick is that we want to import this
function into other deprecated modules, and we want to carry this
subpackage's globals along to prevent some spurious warnings.
Raises
------
AttributeError
If the attribute is not found in ``globals()`` and also could not be
imported from the modules in ``_MODULES``.
Warns
-----
PyVistaDeprecationWarning
If the attribute has been found via importing from the modules in
``_MODULES``, as this implies that the feature has been moved from
``pyvista.utilities``.
"""
from pyvista.core.errors import PyVistaDeprecationWarning # noqa: PLC0415
for module in _MODULES:
feature, import_path = _try_import(module, name)
if feature is not None:
break
else: # pragma: no cover
msg = (
f'Module `pyvista.errors` has been deprecated and we could not automatically '
f'find `{name}`. This feature has moved.'
)
raise AttributeError(msg) from None
message = (
f'The `pyvista.errors` module has been deprecated. '
f'`{name}` is now imported as: `{import_path}`.'
)
warnings.warn(
message,
PyVistaDeprecationWarning,
)
return feature
Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

@@ -0,0 +1,11 @@
"""Examples module."""
from __future__ import annotations
from . import download_3ds as download_3ds
from . import gltf as gltf
from . import planets as planets
from . import vrml as vrml
from .cells import plot_cell as plot_cell
from .downloads import *
from .examples import *
@@ -0,0 +1,843 @@
"""Abstraction layer for downloading, reading, and loading dataset files.
The classes and methods in this module define an API for working with either
a single file or multiple files which may be downloaded and/or loaded as an
example dataset.
Many datasets have a straightforward input to output mapping:
file -> read -> dataset
However, some file formats require multiple input files for reading (e.g.
separate data and header files):
(file1, file1) -> read -> dataset
Or, a dataset may be combination of two separate datasets:
file1 -> read -> dataset1 ┬─> combined_dataset
file2 -> read -> dataset2 ┘
In some cases, the input may be a folder instead of a file (e.g. DICOM):
folder -> read -> dataset
In addition, there may be a need to customize the reading function to read
files with specific options enabled (e.g. set a time value), or perform
post-read processing to modify the dataset (e.g. set active scalars).
This module aims to serve these use cases and provide a flexible way of
downloading, reading, and processing files with a generic mapping:
file or files or folder -> fully processed dataset(s) in any form
"""
# mypy: disable-error-code="redundant-expr"
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Sequence
import functools
import os
from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import Generic
from typing import Protocol
from typing import TypeVar
from typing import Union
from typing import cast
from typing import final
from typing import runtime_checkable
import pyvista as pv
from pyvista.core._typing_core import NumpyArray
from pyvista.core.utilities.fileio import get_ext
if TYPE_CHECKING:
from collections.abc import Callable
# Define TypeVars for two main class definitions used by this module:
# 1. classes for single file inputs: T -> T
# 2. classes for multi-file inputs: (T, ...) -> (T, ...)
# Any properties with these typevars should have a one-to-one mapping for all files
_FilePropStrType_co = TypeVar(
'_FilePropStrType_co',
str,
tuple[str, ...],
covariant=True,
)
_FilePropIntType_co = TypeVar(
'_FilePropIntType_co',
int,
tuple[int, ...],
covariant=True,
)
DatasetObject = Union[pv.DataSet, pv.Texture, NumpyArray[Any], pv.MultiBlock]
DatasetType = Union[
type[pv.DataSet],
type[pv.Texture],
type[NumpyArray[Any]],
type[pv.MultiBlock],
]
class _BaseFilePropsProtocol(Generic[_FilePropStrType_co, _FilePropIntType_co]):
@property
@abstractmethod
def path(self) -> _FilePropStrType_co:
"""Return the path(s) of all files."""
@property
def num_files(self) -> int:
"""Return the number of files from path or paths.
If a path is a folder, the number of files contained in the folder is returned.
"""
path = self.path
paths = [path] if isinstance(path, str) else path
return sum(1 if os.path.isfile(p) else len(_get_all_nested_filepaths(p)) for p in paths)
@property
def unique_extension(self) -> str | tuple[str, ...]:
"""Return the unique file extension(s) from all files."""
return _get_unique_extension(self.path)
@property
@abstractmethod
def _filesize_bytes(self) -> _FilePropIntType_co:
"""Return the file size(s) of all files in bytes."""
@property
@abstractmethod
def _filesize_format(self) -> _FilePropStrType_co:
"""Return the formatted size of all file(s)."""
@property
@abstractmethod
def _total_size_bytes(self) -> int:
"""Return the total size of all files in bytes."""
@property
@abstractmethod
def total_size(self) -> str:
"""Return the total size of all files formatted as a string."""
@property
@abstractmethod
def _reader(
self,
) -> pv.BaseReader | tuple[pv.BaseReader | None, ...] | None:
"""Return the base file reader(s) used to read the files."""
@property
def unique_reader_type(
self,
) -> type[pv.BaseReader] | tuple[type[pv.BaseReader], ...] | None:
"""Return unique reader type(s) from all file readers."""
return _get_unique_reader_type(self._reader)
class _SingleFilePropsProtocol(_BaseFilePropsProtocol[str, int]):
"""Define file properties of a single file."""
class _MultiFilePropsProtocol(
_BaseFilePropsProtocol[tuple[str, ...], tuple[int, ...]],
):
"""Define file properties of multiple files."""
@runtime_checkable
class _Downloadable(Protocol[_FilePropStrType_co]):
"""Class which downloads file(s) from a source."""
@property
@abstractmethod
def source_name(self) -> _FilePropStrType_co:
"""Return the name of the download relative to the base url."""
@property
@abstractmethod
def base_url(self) -> _FilePropStrType_co:
"""Return the base url of the download."""
@property
def source_url_raw(self) -> _FilePropStrType_co:
"""Return the raw source of the download.
This is the full URL used to download the data directly.
"""
name = self.source_name
name_iter = [name] if isinstance(name, str) else name
url = self.base_url
base_url_iter = [url] if isinstance(url, str) else url
url_raw = list(map(os.path.join, base_url_iter, name_iter))
return url_raw[0] if isinstance(name, str) else tuple(url_raw)
@property
def source_url_blob(self) -> _FilePropStrType_co:
"""Return the blob source of the download.
This URL is useful for linking to the source webpage for
a human to open on a browser.
"""
# Make single urls iterable and replace 'raw' with 'blob'
url_raw = self.source_url_raw
url_iter = [url_raw] if isinstance(url_raw, str) else url_raw
url_blob = [url.replace('/raw/', '/blob/') for url in url_iter]
return url_blob[0] if isinstance(url_raw, str) else tuple(url_blob)
@property
@abstractmethod
def path(self) -> _FilePropStrType_co:
"""Return the file path of downloaded file."""
@abstractmethod
def download(self) -> _FilePropStrType_co:
"""Download and return the file path(s)."""
class _DatasetLoader:
"""Load a dataset."""
def __init__(self, load_func: Callable[..., DatasetObject]):
self._load_func = load_func
self._dataset: DatasetObject | None = None
@property
@final
def dataset(self) -> DatasetObject | None:
"""Return the loaded dataset object(s)."""
return self._dataset
def load(self, *args, **kwargs) -> DatasetObject:
"""Load and return the dataset."""
# Subclasses should override this as needed
return self._load_func(*args, **kwargs)
@final
def load_and_store_dataset(self) -> DatasetObject:
"""Load the dataset and store it."""
dataset = self.load()
self._dataset = dataset
return dataset
@final
def clear_dataset(self):
"""Clear the stored dataset object from memory."""
del self._dataset
@property
@final
def dataset_iterable(self) -> tuple[DatasetObject, ...]:
"""Return a tuple of all dataset object(s), including any nested objects.
If the dataset is a MultiBlock, the MultiBlock itself is also returned as the first
item. Any nested MultiBlocks are not included, only their datasets.
E.g. for a composite dataset:
MultiBlock -> (MultiBlock, Block0, Block1, ...)
"""
dataset = self.dataset
def _flat(obj):
if isinstance(obj, Sequence):
output_list = [] # type: ignore[var-annotated]
for item in obj:
(
output_list.extend(item)
if isinstance(item, Sequence)
else output_list.append(item)
)
if any(isinstance(item, Sequence) for item in output_list):
return _flat(output_list)
return output_list
else:
return [obj]
flat = _flat(dataset)
if isinstance(dataset, pv.MultiBlock):
flat.insert(0, dataset)
return tuple(flat)
@property
@final
def unique_dataset_type(
self,
) -> DatasetType | tuple[DatasetType, ...] | None:
"""Return unique dataset type(s) from all datasets."""
return _get_unique_dataset_type(self.dataset_iterable)
@property
@final
def unique_cell_types(
self,
) -> tuple[pv.CellType, ...]:
"""Return unique cell types from all datasets."""
cell_types: dict[pv.CellType, None] = {}
for data in self.dataset_iterable:
# Get the underlying dataset for the texture
dataset = (
cast('pv.ImageData', pv.wrap(data.GetInput()))
if isinstance(data, pv.Texture)
else data
)
try:
if isinstance(dataset, pv.ExplicitStructuredGrid):
# extract_cells_by_type does not support this datatype
# so get cells manually
cells = (c.type for c in dataset.cell)
[cell_types.update({cell_type: None}) for cell_type in cells]
else:
for cell_type in pv.CellType:
extracted = dataset.extract_cells_by_type(cell_type) # type: ignore[union-attr]
if extracted.n_cells > 0:
cell_types[cell_type] = None
except AttributeError:
continue
return tuple(sorted(cell_types.keys()))
class _SingleFile(_SingleFilePropsProtocol):
"""Wrap a single file."""
def __init__(self, path):
from pyvista.examples.downloads import USER_DATA_PATH # noqa: PLC0415
self._path = (
path if path is None or os.path.isabs(path) else os.path.join(USER_DATA_PATH, path)
)
@property
def path(self) -> str:
return self._path
@property
def _filesize_bytes(self) -> int:
return _get_file_or_folder_size(self.path)
@property
def _filesize_format(self) -> str:
return _format_file_size(self._filesize_bytes)
@property
def _total_size_bytes(self) -> int:
return self._filesize_bytes
@property
def total_size(self) -> str:
return self._filesize_format
@property
def _reader(self) -> pv.BaseReader | None:
return None
class _SingleFileDatasetLoader(_SingleFile, _DatasetLoader):
"""Wrap a single file for loading.
Specify the read function and/or load functions for reading and processing the
dataset. The read function is called on the file path first, then, if a load
function is specified, the load function is called on the output from the read
function.
Parameters
----------
path
Path of the file to be loaded.
read_func
Specify the function used to read the file. Defaults to :func:`pyvista.read`.
This can be used for customizing the reader's properties, or using another
read function (e.g. :func:`pyvista.read_texture` for textures). The function
must have the file path as the first argument and should return a dataset.
If default arguments are required by your desired read function, consider
using :class:`functools.partial` to pre-set the arguments before passing it
as an argument to the loader.
load_func
Specify the function used to load the file. Defaults to `None`. This is typically
used to specify any processing of the dataset after reading. The load function
typically will accept a dataset as an input and return a dataset.
"""
def __init__(
self,
path: str,
read_func: Callable[[str], DatasetType] | None = None,
load_func: Callable[[DatasetType], Any] | None = None,
):
_SingleFile.__init__(self, path)
_DatasetLoader.__init__(self, load_func) # type: ignore[arg-type]
self._read_func = pv.read if path and read_func is None else read_func
@property
def _reader(self) -> pv.BaseReader | None:
# TODO: return the actual reader used, and not just a lookup
# (this will require an update to the 'read_func' API)
try:
return pv.get_reader(self.path)
except ValueError:
# Cannot be read directly (requires custom reader)
return None
@property
def path_loadable(self) -> str:
return self.path
def load(self):
path = self.path
read_func = self._read_func
load_func = self._load_func
try:
# Read and load normally
return read_func(path) if load_func is None else load_func(read_func(path)) # type: ignore[misc]
except OSError:
# Handle error generated by pv.read if reading a directory
if read_func is pv.read and Path(path).is_dir():
# Re-define read function to read all files in a directory as a multiblock
read_func = lambda path: _load_as_multiblock( # type: ignore[assignment]
[
_SingleFileDatasetLoader(str(Path(path, fname)))
for fname in sorted(os.listdir(path)) # noqa: PTH208
],
)
return read_func(path) if load_func is None else load_func(read_func(path))
else:
msg = f'Error loading dataset from path:\n\t{self.path}'
raise RuntimeError(msg)
class _DownloadableFile(_SingleFile, _Downloadable[str]):
"""Wrap a single file which must be downloaded.
If downloading a file from an archive, set the filepath of the zip as
``path`` and set ``target_file`` as the file to extract. If the path is
a zip file and no target file is specified, the entire archive is downloaded
and extracted and the root directory of the path is returned.
"""
def __init__(
self,
path: str,
target_file: str | None = None,
):
_SingleFile.__init__(self, path)
from pyvista.examples.downloads import SOURCE # noqa: PLC0415
from pyvista.examples.downloads import USER_DATA_PATH # noqa: PLC0415
from pyvista.examples.downloads import _download_archive_file_or_folder # noqa: PLC0415
from pyvista.examples.downloads import download_file # noqa: PLC0415
from pyvista.examples.downloads import file_from_files # noqa: PLC0415
from pyvista.examples.examples import dir_path # noqa: PLC0415
if Path(path).is_absolute():
# Absolute path must point to a built-in dataset
assert Path(path).parent == Path(
dir_path,
), 'Absolute path must point to a built-in dataset.'
self._base_url = 'https://github.com/pyvista/pyvista/raw/main/pyvista/examples/'
self._source_name = Path(path).name
# the dataset is already downloaded (it's built-in)
# so make download() simply return the local filepath
self._download_func = lambda _: path
else:
# Relative path, use vars from downloads.py
self._base_url = SOURCE
self._download_func = download_file
self._source_name = Path(path).name if Path(path).is_absolute() else path
target_file = '' if target_file is None and (get_ext(path) == '.zip') else target_file
if target_file is not None:
# download from archive
self._download_func = functools.partial(
_download_archive_file_or_folder,
target_file=target_file,
)
# The file path currently points to the archive, not the target file itself
# Try to resolve the full path to the target file (without downloading) if
# the archive already exists in the cache
fullpath = None
if os.path.isfile(self.path):
try:
# Get file path
fullpath = file_from_files(target_file, self.path)
except (FileNotFoundError, RuntimeError):
# Get folder path
fullpath = os.path.join(USER_DATA_PATH, path + '.unzip', target_file)
fullpath = fullpath if os.path.isdir(fullpath) else None
# set the file path as the relative path of the target file if
# the fullpath could not be resolved (i.e. not yet downloaded)
self._path = target_file if fullpath is None else fullpath
@property
def source_name(self) -> str:
return self._source_name
@property
def base_url(self) -> str:
return self._base_url
def download(self) -> str:
path = self._download_func(self._source_name)
assert os.path.isfile(path) or os.path.isdir(path)
# Reset the path since the full path for archive files
# isn't known until after downloading
self._path = path
return path
class _SingleFileDownloadableDatasetLoader(_SingleFileDatasetLoader, _DownloadableFile):
"""Wrap a single file which must first be downloaded and which can also be loaded.
.. warning::
``download()`` should be called before accessing other attributes. Otherwise,
calling ``load()`` or ``path`` may fail or produce unexpected results.
"""
def __init__( # noqa: PLR0917
self,
path: str,
read_func: Callable[[str], DatasetType] | None = None,
load_func: Callable[[DatasetType], DatasetType] | None = None,
target_file: str | None = None,
):
_SingleFileDatasetLoader.__init__(self, path, read_func=read_func, load_func=load_func)
_DownloadableFile.__init__(self, path, target_file=target_file)
class _MultiFileDatasetLoader(_DatasetLoader, _MultiFilePropsProtocol):
"""Wrap multiple files for loading.
Some use cases for loading multi-file examples include:
1. Multiple input files, and each file is read/loaded independently
E.g.: loading two separate datasets for the example
See ``download_bolt_nut`` for a reference implementation.
2. Multiple input files, but only one is read or loaded directly
E.g.: loading a single dataset from a file format where data and metadata are
stored in separate files, such as ``.raw`` and ``.mhd``.
See ``download_head`` for a reference implementation.
3. Multiple input files, all of which make up part of the loaded dataset
E.g.: loading six separate image files for cubemaps
See ``download_sky_box_cube_map`` for a reference implementation.
Parameters
----------
files_func
Specify the function which will return a sequence of :class:`_SingleFile`
objects required for loading the dataset. Alternatively, a directory can be
specified, in which case a separate single-file dataset loader is created
for each file with a default reader.
load_func
Specify the function used to load the files. By default, :meth:`load()` is called
on all the files (if loadable) and a tuple containing the loaded datasets is returned.
"""
def __init__(
self,
files_func: str | Callable[[], Sequence[_SingleFileDatasetLoader | _DownloadableFile]],
load_func: Callable[[Sequence[_SingleFileDatasetLoader]], Any] | None = None,
):
self._files_func = files_func
self._file_loaders_ = None
if load_func is None:
load_func = _load_as_dataset_or_multiblock
_DatasetLoader.__init__(self, load_func)
@property
def _file_objects(self):
if self._file_loaders_ is None and not isinstance(self._files_func, str):
self._file_loaders_ = self._files_func() # type: ignore[assignment]
return self._file_loaders_
@property
def path(self) -> tuple[str, ...]:
return tuple(_flatten_nested_sequence([file.path for file in self._file_objects]))
@property
def path_loadable(self) -> tuple[str, ...]:
return tuple(
file.path for file in self._file_objects if isinstance(file, _SingleFileDatasetLoader)
)
@property
def _filesize_bytes(self) -> tuple[int, ...]:
return tuple(
_flatten_nested_sequence([file._filesize_bytes for file in self._file_objects]),
)
@property
def _filesize_format(self) -> tuple[str, ...]:
return tuple(_format_file_size(size) for size in self._filesize_bytes)
@property
def _total_size_bytes(self) -> int:
return sum(file._total_size_bytes for file in self._file_objects)
@property
def total_size(self) -> str:
return _format_file_size(self._total_size_bytes)
@property
def _reader(
self,
) -> pv.BaseReader | tuple[pv.BaseReader | None, ...] | None:
# TODO: return the actual reader used, and not just a lookup
# (this will require an update to the 'read_func' API)
reader = _flatten_nested_sequence([file._reader for file in self._file_objects])
# flatten in case any file objects themselves are multifiles
reader_out: list[pv.BaseReader] = []
for r in reader:
reader_out.extend(r) if isinstance(r, Sequence) else reader_out.append(r)
return tuple(reader_out)
def load(self):
return self._load_func(self._file_objects)
class _MultiFileDownloadableDatasetLoader(
_MultiFileDatasetLoader,
_Downloadable[tuple[str, ...]],
):
"""Wrap multiple files for downloading and loading."""
@property
def source_name(self) -> tuple[str, ...]:
name = [file.source_name for file in self._file_objects if isinstance(file, _Downloadable)]
return tuple(_flatten_nested_sequence(name))
@property
def base_url(self) -> tuple[str, ...]:
url = [file.base_url for file in self._file_objects if isinstance(file, _Downloadable)]
return tuple(_flatten_nested_sequence(url))
def download(self) -> tuple[str, ...]:
path = [file.download() for file in self._file_objects if isinstance(file, _Downloadable)]
# flatten paths in case any loaders have multiple files
path_out = _flatten_nested_sequence(path)
assert all(os.path.isfile(p) or os.path.isdir(p) for p in path_out)
return tuple(path_out)
_ScalarType = TypeVar('_ScalarType', int, str, pv.BaseReader)
def _flatten_nested_sequence(nested: Sequence[_ScalarType | Sequence[_ScalarType]]):
"""Flatten nested sequences of objects."""
flat: list[_ScalarType] = []
for item in nested:
if isinstance(item, Sequence) and not isinstance(item, str):
flat.extend(item)
else:
flat.append(item)
return flat
def _download_dataset(
dataset_loader: _SingleFileDownloadableDatasetLoader | _MultiFileDownloadableDatasetLoader,
*,
load: bool = True,
metafiles: bool = False,
):
"""Download and load a dataset file or files.
Parameters
----------
dataset_loader
SingleFile or MultiFile object(s) of the dataset(s) to download or load.
load
Read and load the file after downloading. When ``False``,
return the path or paths to the example's file(s).
metafiles
When ``load`` is ``False``, set this value to ``True`` to
return all files required to load the example, including any metafiles.
If ``False``, only the paths of files which are explicitly loaded are
returned. E.g if a file format uses two files to specify the header info
and file data separately, setting ``metafiles=True`` will return a tuple
with both file paths, whereas setting ``metafiles=False`` will only return
the single path of the header file as a string.
Returns
-------
Any
Loaded dataset or path(s) to the example's files depending on the ``load``
parameter. Dataset may be a texture, mesh, multiblock, array, tuple of meshes,
or any other output loaded by the example.
"""
# Download all files for the dataset, include any metafiles
path = dataset_loader.download()
# Exclude non-loadable metafiles from result (if any)
if not metafiles and isinstance(dataset_loader, _MultiFileDownloadableDatasetLoader):
path = dataset_loader.path_loadable
# Return scalar if only one loadable file
path = path[0] if len(path) == 1 else path
return dataset_loader.load() if load else path
def _load_as_multiblock(
files: Sequence[_SingleFileDatasetLoader | _MultiFileDatasetLoader],
names: Sequence[str] | None = None,
) -> pv.MultiBlock:
"""Load multiple files as a MultiBlock.
This function can be used as a loading function for :class:`MultiFileLoadable`
If the use of the ``names`` parameter is needed, use :class:`functools.partial`
to partially specify the names parameter before passing it as loading function.
"""
multi = pv.MultiBlock()
if names is None:
# set names, use filename without ext by default or dirname
paths = _flatten_nested_sequence(
[file.path_loadable for file in files if isinstance(file, _DatasetLoader)],
)
paths = [Path(path) for path in paths]
names = [
path.name[: -len(get_ext(path.name))] if path.is_file() else path.name
for path in paths
]
for file, name in zip(files, names):
if not isinstance(file, _DatasetLoader):
continue # type: ignore[unreachable]
loaded = file.load()
assert isinstance(
loaded,
(pv.MultiBlock, pv.DataSet),
), (
f'Only MultiBlock or DataSet objects can be loaded as a MultiBlock. '
f"Got {type(loaded)}.'"
)
multi.append(loaded, name)
return multi
def _load_as_cubemap(files: str | _SingleFile | Sequence[_SingleFile]) -> pv.Texture:
"""Load multiple files as a cubemap.
Input may be a single directory with 6 cubemap files, or a sequence
of 6 files
"""
path = (
files
if isinstance(files, str)
else (files.path if isinstance(files, _SingleFile) else [file.path for file in files])
)
return (
pv.cubemap(path)
if isinstance(files, str) and os.path.isdir(files)
else pv.cubemap_from_filenames(path)
)
def _load_as_dataset_or_multiblock(files):
multiblock = _load_as_multiblock(files)
return multiblock[0] if len(multiblock) == 1 else multiblock
def _load_and_merge(files: Sequence[_SingleFile]):
"""Load all loadable files as separate datasets and merge them."""
loaded = [file.load() for file in files if isinstance(file, _DatasetLoader)]
assert len(loaded) > 0
return pv.merge(loaded)
def _get_file_or_folder_size(filepath) -> int:
if os.path.isfile(filepath):
return os.path.getsize(filepath)
assert os.path.isdir(filepath), 'Expected a file or folder path.'
all_filepaths = _get_all_nested_filepaths(filepath)
return sum(os.path.getsize(file) for file in all_filepaths)
def _format_file_size(size: int) -> str:
size_flt = float(size)
for unit in ('B', 'KB', 'MB'):
if round(size_flt * 10) / 10 < 1000.0:
return f'{int(size_flt)} {unit}' if unit == 'B' else f'{size_flt:3.1f} {unit}'
size_flt /= 1000.0
return f'{size_flt:.1f} GB'
def _get_file_or_folder_ext(path: str):
"""Wrap the `get_ext` function to handle special cases for directories."""
if os.path.isfile(path):
return get_ext(path)
assert os.path.isdir(path), 'Expected a file or folder path.'
all_paths = _get_all_nested_filepaths(path)
ext = [get_ext(file) for file in all_paths]
assert len(ext) != 0, f'No files with extensions were found in"\n\t{path}'
return ext
def _get_all_nested_filepaths(filepath, *, exclude_readme=True):
"""Walk through directory and get all file paths.
Optionally exclude any readme files (if any).
"""
assert os.path.isfile(filepath) or os.path.isdir(filepath)
condition = lambda name: True if not exclude_readme else not name.lower().startswith('readme')
return next(
[os.path.join(path, name) for name in files if condition(name)]
for path, _, files in os.walk(filepath)
)
def _get_unique_extension(path: str | Sequence[str]):
"""Return a file extension or unique set of file extensions from a path or paths."""
ext_set = set()
fname_sequence = [path] if isinstance(path, str) else path
# Add all file extensions to the set
for file in fname_sequence:
ext = _get_file_or_folder_ext(file)
ext_set.add(ext) if isinstance(ext, str) else ext_set.update(ext)
# Format output
ext_output = tuple(ext_set)
return ext_output[0] if len(ext_output) == 1 else tuple(sorted(ext_output))
def _get_unique_reader_type(
reader: pv.BaseReader | tuple[pv.BaseReader | None, ...] | None,
) -> type[pv.BaseReader] | tuple[type[pv.BaseReader], ...] | None:
"""Return a reader type or tuple of unique reader types."""
if reader is None or (isinstance(reader, Sequence) and all(r is None for r in reader)):
return None
reader_set: set[type[pv.BaseReader]] = set()
reader_type = (
[type(reader)]
if not isinstance(reader, Sequence)
else [type(r) for r in reader if r is not None]
)
# Add all reader types to the set
reader_set.update(reader_type)
# Format output
reader_output = tuple(reader_set)
return reader_output[0] if len(reader_output) == 1 else tuple(reader_output)
def _get_unique_dataset_type(
dataset_iterable: tuple[DatasetObject, ...],
) -> DatasetType | tuple[DatasetType, ...]:
"""Return a dataset type or tuple of unique dataset types."""
dataset_types: dict[DatasetType, None] = {} # use dict as an ordered set
for dataset in dataset_iterable:
dataset_types[type(dataset)] = None
output = tuple(dataset_types.keys())
return output[0] if len(output) == 1 else output
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,28 @@
"""Contains 3ds examples."""
from __future__ import annotations
from .downloads import download_file
def download_iflamigm(): # pragma: no cover
"""Download a iflamigm image.
.. versionadded:: 0.44.0
Returns
-------
str
Filename of the 3DS file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> download_3ds_file = examples.download_3ds.download_iflamigm()
>>> pl = pv.Plotter()
>>> pl.import_3ds(download_3ds_file)
>>> pl.show()
"""
return download_file('iflamigm.3ds')
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,798 @@
"""Built-in examples that ship with PyVista and do not need to be downloaded.
Examples
--------
>>> from pyvista import examples
>>> mesh = examples.load_ant()
>>> mesh.plot()
"""
from __future__ import annotations
import math
import os
from pathlib import Path
import numpy as np
import pyvista
from pyvista.examples._dataset_loader import _DatasetLoader
from pyvista.examples._dataset_loader import _SingleFileDownloadableDatasetLoader
# get location of this folder and the example files
dir_path = str(Path(os.path.realpath(__file__)).parent)
antfile = str(Path(dir_path) / 'ant.ply')
planefile = str(Path(dir_path) / 'airplane.ply')
hexbeamfile = str(Path(dir_path) / 'hexbeam.vtk')
spherefile = str(Path(dir_path) / 'sphere.ply')
uniformfile = str(Path(dir_path) / 'uniform.vtk')
rectfile = str(Path(dir_path) / 'rectilinear.vtk')
globefile = str(Path(dir_path) / 'globe.vtk')
mapfile = str(Path(dir_path) / '2k_earth_daymap.jpg')
channelsfile = str(Path(dir_path) / 'channels.vti')
logofile = str(Path(dir_path) / 'pyvista_logo.png')
nutfile = str(Path(dir_path) / 'nut.ply')
frogtissuesfile = str(Path(dir_path) / 'frog_tissues.vti')
def load_ant():
"""Load ply ant mesh.
Returns
-------
pyvista.PolyData
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_ant()
>>> dataset.plot()
.. seealso::
:ref:`Ant Dataset <ant_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_ant.load()
_dataset_ant = _SingleFileDownloadableDatasetLoader(antfile, read_func=pyvista.PolyData) # type: ignore[arg-type]
def load_airplane():
"""Load ply airplane mesh.
Returns
-------
pyvista.PolyData
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_airplane()
>>> dataset.plot()
.. seealso::
:ref:`Airplane Dataset <airplane_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_airplane.load()
_dataset_airplane = _SingleFileDownloadableDatasetLoader(planefile, read_func=pyvista.PolyData) # type: ignore[arg-type]
def load_sphere():
"""Load sphere ply mesh.
Returns
-------
pyvista.PolyData
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_sphere()
>>> dataset.plot()
.. seealso::
:ref:`Sphere Dataset <sphere_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_sphere.load()
_dataset_sphere = _SingleFileDownloadableDatasetLoader(spherefile, read_func=pyvista.PolyData) # type: ignore[arg-type]
def load_uniform():
"""Load a sample uniform grid.
Returns
-------
pyvista.ImageData
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_uniform()
>>> dataset.plot()
.. seealso::
:ref:`Uniform Dataset <uniform_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_uniform.load()
_dataset_uniform = _SingleFileDownloadableDatasetLoader(uniformfile, read_func=pyvista.ImageData) # type: ignore[arg-type]
def load_rectilinear():
"""Load a sample uniform grid.
Returns
-------
pyvista.RectilinearGrid
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_rectilinear()
>>> dataset.plot()
.. seealso::
:ref:`Rectilinear Dataset <rectilinear_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_rectilinear.load()
_dataset_rectilinear = _SingleFileDownloadableDatasetLoader(
rectfile,
read_func=pyvista.RectilinearGrid, # type: ignore[arg-type]
)
def load_hexbeam():
"""Load a sample UnstructuredGrid.
Returns
-------
pyvista.UnstructuredGrid
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_hexbeam()
>>> dataset.plot()
.. seealso::
:ref:`Hexbeam Dataset <hexbeam_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_hexbeam.load()
_dataset_hexbeam = _SingleFileDownloadableDatasetLoader(
hexbeamfile,
read_func=pyvista.UnstructuredGrid, # type: ignore[arg-type]
)
def load_tetbeam():
"""Load a sample UnstructuredGrid containing only tetrahedral cells.
Returns
-------
pyvista.UnstructuredGrid
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_tetbeam()
>>> dataset.plot()
.. seealso::
:ref:`Tetbeam Dataset <tetbeam_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_tetbeam.load()
def _tetbeam_load_func():
# make the geometry identical to the hexbeam
xrng = np.linspace(0, 1, 3)
yrng = np.linspace(0, 1, 3)
zrng = np.linspace(0, 5, 11)
grid = pyvista.RectilinearGrid(xrng, yrng, zrng)
return grid.to_tetrahedra()
_dataset_tetbeam = _DatasetLoader(_tetbeam_load_func)
def load_structured():
"""Load a simple StructuredGrid.
Returns
-------
pyvista.StructuredGrid
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_structured()
>>> dataset.plot()
.. seealso::
:ref:`Structured Dataset <structured_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_structured.load()
def _structured_load_func():
x = np.arange(-10, 10, 0.25)
y = np.arange(-10, 10, 0.25)
x, y = np.meshgrid(x, y)
r = np.sqrt(x**2 + y**2)
z = np.sin(r)
return pyvista.StructuredGrid(x, y, z)
_dataset_structured = _DatasetLoader(_structured_load_func)
def load_globe():
"""Load a globe source.
Returns
-------
pyvista.PolyData
Globe dataset with earth texture.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_globe()
>>> texture = examples.load_globe_texture()
>>> dataset.plot(texture=texture)
.. seealso::
:ref:`Globe Dataset <globe_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_globe.load()
_dataset_globe = _SingleFileDownloadableDatasetLoader(globefile, read_func=pyvista.PolyData) # type: ignore[arg-type]
def load_globe_texture():
"""Load a pyvista.Texture that can be applied to the globe source.
Returns
-------
pyvista.Texture
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_globe_texture()
>>> dataset.plot()
.. seealso::
:ref:`Globe Texture Dataset <globe_texture_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_globe_texture.load()
_dataset_globe_texture = _SingleFileDownloadableDatasetLoader(
mapfile,
read_func=pyvista.read_texture, # type: ignore[arg-type]
)
def load_channels():
"""Load a uniform grid of fluvial channels in the subsurface.
Returns
-------
pyvista.ImageData
Dataset.
Examples
--------
>>> from pyvista import examples
>>> dataset = examples.load_channels()
>>> dataset.plot()
.. seealso::
:ref:`Channels Dataset <channels_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_channels.load()
_dataset_channels = _SingleFileDownloadableDatasetLoader(channelsfile)
def load_spline():
"""Load an example spline mesh.
This example data was created with:
.. code-block:: python
>>> import numpy as np
>>> import pyvista as pv
>>> theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
>>> z = np.linspace(-2, 2, 100)
>>> r = z**2 + 1
>>> x = r * np.sin(theta)
>>> y = r * np.cos(theta)
>>> points = np.column_stack((x, y, z))
>>> mesh = pv.Spline(points, 1000)
Returns
-------
pyvista.PolyData
Spline mesh.
Examples
--------
>>> from pyvista import examples
>>> spline = examples.load_spline()
>>> spline.plot()
.. seealso::
:ref:`Spline Dataset <spline_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_spline.load()
def _spline_load_func():
theta = np.linspace(-4 * np.pi, 4 * np.pi, 100)
z = np.linspace(-2, 2, 100)
r = z**2 + 1
x = r * np.sin(theta)
y = r * np.cos(theta)
points = np.column_stack((x, y, z))
return pyvista.Spline(points, 1000)
_dataset_spline = _DatasetLoader(_spline_load_func)
def load_random_hills():
"""Create random hills toy example.
Uses the parametric random hill function to create hills oriented
like topography and adds an elevation array.
This example dataset was created with:
.. code-block:: python
>>> mesh = pv.ParametricRandomHills() # doctest:+SKIP
>>> mesh = mesh.elevation() # doctest:+SKIP
Returns
-------
pyvista.PolyData
Random hills mesh.
Examples
--------
>>> from pyvista import examples
>>> mesh = examples.load_random_hills()
>>> mesh.plot()
.. seealso::
:ref:`Random Hills Dataset <random_hills_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_random_hills.load()
def _random_hills_load_func():
mesh = pyvista.ParametricRandomHills()
return mesh.elevation()
_dataset_random_hills = _DatasetLoader(_random_hills_load_func)
def load_sphere_vectors():
"""Create example sphere with a swirly vector field defined on nodes.
Returns
-------
pyvista.PolyData
Mesh containing vectors.
Examples
--------
>>> from pyvista import examples
>>> mesh = examples.load_sphere_vectors()
>>> mesh.point_data
pyvista DataSetAttributes
Association : POINT
Active Scalars : vectors
Active Vectors : vectors
Active Texture : None
Active Normals : Normals
Contains arrays :
Normals float32 (842, 3) NORMALS
vectors float32 (842, 3) VECTORS
.. seealso::
:ref:`Sphere Vectors Dataset <sphere_vectors_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_sphere_vectors.load()
def _sphere_vectors_load_func() -> pyvista.PolyData:
sphere = pyvista.Sphere(radius=math.pi)
# make cool swirly pattern
vectors = np.vstack(
(
np.sin(sphere.points[:, 0]),
np.cos(sphere.points[:, 1]),
np.cos(sphere.points[:, 2]),
),
).T
# add and scale
sphere['vectors'] = vectors * 0.3
sphere.set_active_vectors('vectors')
return sphere
_dataset_sphere_vectors = _DatasetLoader(_sphere_vectors_load_func)
def load_explicit_structured(dimensions=(5, 6, 7), spacing=(20, 10, 1)):
"""Load a simple explicit structured grid.
Parameters
----------
dimensions : tuple(int), optional
Grid dimensions. Default is (5, 6, 7).
spacing : tuple(int), optional
Grid spacing. Default is (20, 10, 1).
Returns
-------
pyvista.ExplicitStructuredGrid
An explicit structured grid.
Examples
--------
>>> from pyvista import examples
>>> grid = examples.load_explicit_structured()
>>> grid.plot(show_edges=True)
.. seealso::
:ref:`Explicit Structured Dataset <explicit_structured_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_explicit_structured.load(dimensions=dimensions, spacing=spacing)
def _explicit_structured_load_func(dimensions=(5, 6, 7), spacing=(20, 10, 1)):
ni, nj, nk = np.asarray(dimensions) - 1
si, sj, sk = spacing
xi = np.arange(0.0, (ni + 1) * si, si)
yi = np.arange(0.0, (nj + 1) * sj, sj)
zi = np.arange(0.0, (nk + 1) * sk, sk)
return pyvista.StructuredGrid(
*np.meshgrid(xi, yi, zi, indexing='ij')
).cast_to_explicit_structured_grid()
_dataset_explicit_structured = _DatasetLoader(_explicit_structured_load_func)
def load_nut():
"""Load an example nut mesh.
Returns
-------
pyvista.PolyData
A sample nut surface dataset.
Examples
--------
Load an example nut and plot with smooth shading.
>>> from pyvista import examples
>>> mesh = examples.load_nut()
>>> mesh.plot(smooth_shading=True, split_sharp_edges=True)
.. seealso::
:ref:`Nut Dataset <nut_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_nut.load()
_dataset_nut = _SingleFileDownloadableDatasetLoader(nutfile)
def load_hydrogen_orbital(n=1, l=0, m=0, zoom_fac=1.0): # noqa: PLR0917
"""Load the hydrogen wave function for a :class:`pyvista.ImageData`.
This is the solution to the Schrödinger equation for hydrogen
evaluated in three-dimensional Cartesian space.
Inspired by `Hydrogen Wave Function
<http://staff.ustc.edu.cn/~zqj/posts/Hydrogen-Wavefunction/>`_.
Parameters
----------
n : int, default: 1
Principal quantum number. Must be a positive integer. This is often
referred to as the "energy level" or "shell".
l : int, default: 0
Azimuthal quantum number. Must be a non-negative integer strictly
smaller than ``n``. By convention this value is represented by the
letters s, p, d, f, etc.
m : int, default: 0
Magnetic quantum number. Must be an integer ranging from ``-l`` to
``l`` (inclusive). This is the orientation of the angular momentum in
space.
zoom_fac : float, default: 1.0
Zoom factor for the electron cloud. Increase this value to focus on the
center of the electron cloud.
Returns
-------
pyvista.ImageData
ImageData containing two ``point_data`` arrays:
* ``'real_wf'`` - Real part of the wave function.
* ``'wf'`` - Complex wave function.
Notes
-----
This example requires `sympy <https://www.sympy.org/>`_.
Examples
--------
Plot the 3dxy orbital of a hydrogen atom. This corresponds to the quantum
numbers ``n=3``, ``l=2``, and ``m=-2``.
>>> from pyvista import examples
>>> grid = examples.load_hydrogen_orbital(3, 2, -2)
>>> grid.plot(volume=True, opacity=[1, 0, 1], cmap='magma')
See :ref:`atomic_orbitals_example` for additional examples using
this function.
.. seealso::
:ref:`Hydrogen Orbital Dataset <hydrogen_orbital_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_hydrogen_orbital.load(n=n, l=l, m=m, zoom_fac=zoom_fac)
def _hydrogen_orbital_load_func(n=1, l=0, m=0, zoom_fac=1.0): # noqa: PLR0917
try:
from sympy import lambdify
from sympy.abc import phi
from sympy.abc import r
from sympy.abc import theta
from sympy.physics.hydrogen import Psi_nlm
except ImportError: # pragma: no cover
msg = '\n\nInstall sympy to run this example. Run:\n\n pip install sympy\n'
raise ImportError(msg) from None
if n < 1:
msg = '`n` must be at least 1'
raise ValueError(msg)
if l not in range(n):
msg = f'`l` must be one of: {list(range(n))}'
raise ValueError(msg)
if m not in range(-l, l + 1):
msg = f'`m` must be one of: {list(range(-l, l + 1))}'
raise ValueError(msg)
psi = lambdify((r, phi, theta), Psi_nlm(n, l, m, r, phi, theta, 1), 'numpy')
org = 1.5 * n**2 + 1.0 if n == 1 else 1.5 * n**2 + 10.0
org /= zoom_fac
dim = 100
sp = (org * 2) / (dim - 1)
grid = pyvista.ImageData(
dimensions=(dim, dim, dim),
spacing=(sp, sp, sp),
origin=(-org, -org, -org),
)
r, theta, phi = pyvista.cartesian_to_spherical(grid.x, grid.y, grid.z)
wfc = psi(r, phi, theta).reshape(grid.dimensions)
grid['real_wf'] = np.real(wfc.ravel())
grid['wf'] = wfc.ravel()
return grid
_dataset_hydrogen_orbital = _DatasetLoader(_hydrogen_orbital_load_func)
def load_logo():
"""Load the PyVista logo as a :class:`pyvista.ImageData`.
.. note::
Alternative versions of the logo file are also available from the ``logo``
directory at https://github.com/pyvista/pyvista/. This includes
higher-resolution ``.png`` files and a vectorized ``.svg`` version.
.. versionchanged:: 0.45
The dimensions of the image is now ``1389 x 592``.
Previously, it was ``1920 x 718``.
Returns
-------
pyvista.ImageData
ImageData of the PyVista logo.
Examples
--------
>>> from pyvista import examples
>>> image = examples.load_logo()
>>> image.dimensions
(1389, 592, 1)
>>> image.plot(cpos='xy', zoom='tight', rgb=True, show_axes=False)
.. seealso::
:ref:`Logo Dataset <logo_dataset>`
See this dataset in the Dataset Gallery for more info.
"""
return _dataset_logo.load()
_dataset_logo = _SingleFileDownloadableDatasetLoader(logofile)
def load_frog_tissues():
"""Load frog tissues dataset.
This dataset contains tissue segmentation labels for the frog dataset.
.. versionadded:: 0.44.0
Returns
-------
pyvista.ImageData
Dataset.
Examples
--------
Load data
>>> import numpy as np
>>> import pyvista as pv
>>> from pyvista import examples
>>> data = examples.load_frog_tissues()
Plot tissue labels as a volume
First, define plotting parameters
>>> # Configure colors / color bar
>>> clim = data.get_data_range() # Set color bar limits to match data
>>> cmap = 'glasbey' # Use a categorical colormap
>>> categories = True # Ensure n_colors matches number of labels
>>> opacity = 'foreground' # Make foreground opaque, background transparent
>>> opacity_unit_distance = 1
Set plotting resolution to half the image's spacing
>>> res = np.array(data.spacing) / 2
Define rendering parameters
>>> mapper = 'gpu'
>>> shade = True
>>> ambient = 0.3
>>> diffuse = 0.6
>>> specular = 0.5
>>> specular_power = 40
Make and show plot
>>> p = pv.Plotter()
>>> _ = p.add_volume(
... data,
... clim=clim,
... ambient=ambient,
... shade=shade,
... diffuse=diffuse,
... specular=specular,
... specular_power=specular_power,
... mapper=mapper,
... opacity=opacity,
... opacity_unit_distance=opacity_unit_distance,
... categories=categories,
... cmap=cmap,
... resolution=res,
... )
>>> p.camera_position = 'yx' # Set camera to provide a dorsal view
>>> p.show()
.. seealso::
:ref:`Frog Tissues Dataset <frog_tissues_dataset>`
See this dataset in the Dataset Gallery for more info.
:ref:`Frog Dataset <frog_dataset>`
:ref:`medical_dataset_gallery`
Browse other medical datasets.
"""
return _dataset_frog_tissues.load()
_dataset_frog_tissues = _SingleFileDownloadableDatasetLoader(frogtissuesfile)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,139 @@
"""Contains glTF examples."""
from __future__ import annotations
import pooch
from .downloads import USER_DATA_PATH
GLTF_FETCHER = pooch.create(
path=USER_DATA_PATH,
base_url='https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/master/2.0/',
registry={
'Avocado/glTF-Binary/Avocado.glb': None,
'CesiumMilkTruck/glTF-Binary/CesiumMilkTruck.glb': None,
'DamagedHelmet/glTF-Embedded/DamagedHelmet.gltf': None,
'GearboxAssy/glTF-Binary/GearboxAssy.glb': None,
'SheenChair/glTF-Binary/SheenChair.glb': None,
},
retry_if_failed=3,
)
def download_damaged_helmet(): # pragma: no cover
"""Download the damaged helmet example.
Files hosted at https://github.com/KhronosGroup/glTF-Sample-Models
Returns
-------
str
Filename of the gltf file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> gltf_file = examples.gltf.download_damaged_helmet()
>>> cubemap = examples.download_sky_box_cube_map()
>>> pl = pv.Plotter()
>>> pl.import_gltf(gltf_file)
>>> pl.set_environment_texture(cubemap)
>>> pl.show()
"""
return GLTF_FETCHER.fetch('DamagedHelmet/glTF-Embedded/DamagedHelmet.gltf')
def download_sheen_chair(): # pragma: no cover
"""Download the sheen chair example.
Files hosted at https://github.com/KhronosGroup/glTF-Sample-Models
Returns
-------
str
Filename of the gltf file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> gltf_file = examples.gltf.download_sheen_chair()
>>> cubemap = examples.download_sky_box_cube_map()
>>> pl = pv.Plotter() # doctest:+SKIP
>>> pl.import_gltf(gltf_file) # doctest:+SKIP
>>> pl.set_environment_texture(cubemap) # doctest:+SKIP
>>> pl.show() # doctest:+SKIP
"""
return GLTF_FETCHER.fetch('SheenChair/glTF-Binary/SheenChair.glb')
def download_gearbox(): # pragma: no cover
"""Download the gearbox example.
Files hosted at https://github.com/KhronosGroup/glTF-Sample-Models
Returns
-------
str
Filename of the gltf file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> gltf_file = examples.gltf.download_gearbox()
>>> pl = pv.Plotter()
>>> pl.import_gltf(gltf_file)
>>> pl.show()
"""
return GLTF_FETCHER.fetch('GearboxAssy/glTF-Binary/GearboxAssy.glb')
def download_avocado(): # pragma: no cover
"""Download the avocado example.
Files hosted at https://github.com/KhronosGroup/glTF-Sample-Models
Returns
-------
str
Filename of the gltf file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> gltf_file = examples.gltf.download_avocado()
>>> pl = pv.Plotter()
>>> pl.import_gltf(gltf_file)
>>> pl.show()
"""
return GLTF_FETCHER.fetch('Avocado/glTF-Binary/Avocado.glb')
def download_milk_truck(): # pragma: no cover
"""Download the milk truck example.
Files hosted at https://github.com/KhronosGroup/glTF-Sample-Models
Returns
-------
str
Filename of the gltf file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> gltf_file = examples.gltf.download_milk_truck()
>>> pl = pv.Plotter()
>>> pl.import_gltf(gltf_file)
>>> pl.show()
"""
return GLTF_FETCHER.fetch('CesiumMilkTruck/glTF-Binary/CesiumMilkTruck.glb')
@@ -0,0 +1,159 @@
# vtk DataFile Version 4.1
vtk output
ASCII
DATASET UNSTRUCTURED_GRID
POINTS 99 double
0 0 0 1 0 0 0.5 0 0
1 1 0 1 0.5 0 0 1 0
0.5 1 0 0 0.5 0 0.5 0.5 0
0 0 5 1 0 5 0.5 0 5
1 1 5 1 0.5 5 0 1 5
0.5 1 5 0 0.5 5 0.5 0.5 5
1 0 0.5 1 0 1 1 0 1.5
1 0 2 1 0 2.5 1 0 3
1 0 3.5 1 0 4 1 0 4.5
0 0 0.5 0 0 1 0 0 1.5
0 0 2 0 0 2.5 0 0 3
0 0 3.5 0 0 4 0 0 4.5
0.5 0 0.5 0.5 0 1 0.5 0 1.5
0.5 0 2 0.5 0 2.5 0.5 0 3
0.5 0 3.5 0.5 0 4 0.5 0 4.5
1 1 0.5 1 1 1 1 1 1.5
1 1 2 1 1 2.5 1 1 3
1 1 3.5 1 1 4 1 1 4.5
1 0.5 0.5 1 0.5 1 1 0.5 1.5
1 0.5 2 1 0.5 2.5 1 0.5 3
1 0.5 3.5 1 0.5 4 1 0.5 4.5
0 1 0.5 0 1 1 0 1 1.5
0 1 2 0 1 2.5 0 1 3
0 1 3.5 0 1 4 0 1 4.5
0.5 1 0.5 0.5 1 1 0.5 1 1.5
0.5 1 2 0.5 1 2.5 0.5 1 3
0.5 1 3.5 0.5 1 4 0.5 1 4.5
0 0.5 0.5 0 0.5 1 0 0.5 1.5
0 0.5 2 0 0.5 2.5 0 0.5 3
0 0.5 3.5 0 0.5 4 0 0.5 4.5
0.5 0.5 0.5 0.5 0.5 1 0.5 0.5 1.5
0.5 0.5 2 0.5 0.5 2.5 0.5 0.5 3
0.5 0.5 3.5 0.5 0.5 4 0.5 0.5 4.5
CELLS 40 360
8 0 2 8 7 27 36 90 81
8 2 1 4 8 36 18 54 90
8 7 8 6 5 81 90 72 63
8 8 4 3 6 90 54 45 72
8 27 36 90 81 28 37 91 82
8 36 18 54 90 37 19 55 91
8 81 90 72 63 82 91 73 64
8 90 54 45 72 91 55 46 73
8 28 37 91 82 29 38 92 83
8 37 19 55 91 38 20 56 92
8 82 91 73 64 83 92 74 65
8 91 55 46 73 92 56 47 74
8 29 38 92 83 30 39 93 84
8 38 20 56 92 39 21 57 93
8 83 92 74 65 84 93 75 66
8 92 56 47 74 93 57 48 75
8 30 39 93 84 31 40 94 85
8 39 21 57 93 40 22 58 94
8 84 93 75 66 85 94 76 67
8 93 57 48 75 94 58 49 76
8 31 40 94 85 32 41 95 86
8 40 22 58 94 41 23 59 95
8 85 94 76 67 86 95 77 68
8 94 58 49 76 95 59 50 77
8 32 41 95 86 33 42 96 87
8 41 23 59 95 42 24 60 96
8 86 95 77 68 87 96 78 69
8 95 59 50 77 96 60 51 78
8 33 42 96 87 34 43 97 88
8 42 24 60 96 43 25 61 97
8 87 96 78 69 88 97 79 70
8 96 60 51 78 97 61 52 79
8 34 43 97 88 35 44 98 89
8 43 25 61 97 44 26 62 98
8 88 97 79 70 89 98 80 71
8 97 61 52 79 98 62 53 80
8 35 44 98 89 9 11 17 16
8 44 26 62 98 11 10 13 17
8 89 98 80 71 16 17 15 14
8 98 62 53 80 17 13 12 15
CELL_TYPES 40
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
12
CELL_DATA 40
SCALARS sample_cell_scalars int
LOOKUP_TABLE default
1 2 3 4 5 6 7 8 9
10 11 12 13 14 15 16 17 18
19 20 21 22 23 24 25 26 27
28 29 30 31 32 33 34 35 36
37 38 39 40
POINT_DATA 99
FIELD FieldData 2
sample_point_scalars 1 99 vtktypeint64
1 2 4 6 8 10 12 15 19
22 23 25 27 29 31 33 36 40
44 46 48 50 52 54 56 58 60
63 65 67 69 71 73 75 77 79
91 93 95 97 99 101 103 105 107
119 121 123 125 127 129 131 133 135
147 149 151 153 155 157 159 161 163
175 177 179 181 183 185 187 189 191
203 205 207 209 211 213 215 217 219
240 242 244 246 248 250 252 254 256
286 288 290 292 294 296 298 300 302
VTKorigID 1 99 vtktypeint64
0 1 2 3 4 5 6 7 8
9 10 11 12 13 14 15 16 17
18 19 20 21 22 23 24 25 26
27 28 29 30 31 32 33 34 35
36 37 38 39 40 41 42 43 44
45 46 47 48 49 50 51 52 53
54 55 56 57 58 59 60 61 62
63 64 65 66 67 68 69 70 71
72 73 74 75 76 77 78 79 80
81 82 83 84 85 86 87 88 89
90 91 92 93 94 95 96 97 98
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,75 @@
"""Contains vrml examples."""
from __future__ import annotations
from pyvista.examples.downloads import download_file
def download_teapot(): # pragma: no cover
"""Download the a 2-manifold solid version of the famous teapot example.
Returns
-------
str
Filename of the VRML file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> vrml_file = examples.vrml.download_teapot()
>>> pl = pv.Plotter()
>>> pl.import_vrml(vrml_file)
>>> pl.show()
"""
return download_file('vrml/teapot.wrl')
def download_sextant(): # pragma: no cover
"""Download the sextant example.
Returns
-------
str
Filename of the VRML file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> vrml_file = examples.vrml.download_sextant()
>>> pl = pv.Plotter()
>>> pl.import_vrml(vrml_file)
>>> pl.show()
"""
return download_file('vrml/sextant.wrl')
def download_grasshopper(): # pragma: no cover
"""Download the grasshoper example.
.. versionadded:: 0.45
Returns
-------
str
Filename of the VRML file.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import examples
>>> vrml_file = examples.vrml.download_grasshopper()
>>> pl = pv.Plotter()
>>> pl.import_vrml(vrml_file)
>>> pl.camera_position = [
... (25.0, 32.0, 44.0),
... (0.0, 0.931, -6.68),
... (-0.20, 0.90, -0.44),
... ]
>>> pl.show()
"""
return download_file('grasshopper/grasshop.wrl')
@@ -0,0 +1 @@
"""Module level init for ``pyvista.ext``."""
@@ -0,0 +1,736 @@
"""Plot directive module.
A directive for including a PyVista plot in a Sphinx document.
The ``.. pyvista-plot::`` sphinx directive will include an inline
``.png`` image.
The source code for the plot may be included in one of two ways:
1. Using **doctest** syntax::
.. pyvista-plot::
>>> import pyvista as pv
>>> sphere = pv.Sphere()
>>> out = sphere.plot()
2. **A path to a source file** as the argument to the directive::
.. pyvista-plot:: path/to/plot.py
When a path to a source file is given, the content of the
directive may optionally contain a caption for the plot::
.. pyvista-plot:: path/to/plot.py
The plot's caption.
Additionally, one may specify the name of a function to call (with
no arguments) immediately after importing the module::
.. pyvista-plot:: path/to/plot.py plot_function1
.. note::
Code blocks containing ``doctest:+SKIP`` will be skipped.
.. note::
Animations will not be saved, only the last frame will be shown.
**Options**
The ``pyvista-plot`` directive supports the following options:
include-source : bool
Whether to display the source code. The default can be changed
using the ``pyvista_plot_include_source`` variable in :file:`conf.py`.
encoding : str
If this source file is in a non-UTF8 or non-ASCII encoding, the
encoding must be specified using the ``:encoding:`` option. The
encoding will not be inferred using the ``-*- coding -*-`` metacomment.
context : None
If provided, the code will be run in the context of all previous plot
directives for which the ``:context:`` option was specified. This only
applies to inline code plot directives, not those run from files.
nofigs : None
When setting this flag, the code block will be run but no figures will be
inserted. This is usually useful with the ``:context:`` option.
caption : str
If specified, the option's argument will be used as a caption for the
figure. This overwrites the caption given in the content, when the plot
is generated from a file.
force_static : None
When setting this flag, static images will be used instead of an
interactive scene.
skip : bool, default: True
Whether to skip execution of this directive. If no argument is provided
i.e., ``:skip:``, then it defaults to ``:skip: true``. Default
behaviour is controlled by the ``plot_skip`` boolean variable in
:file:`conf.py`. Note that, if specified, this option overrides the
``plot_skip`` configuration.
optional : None
This flag marks the directive for *conditional* execution. Whether the
directive is executed is controlled by the ``plot_skip_optional``
boolean variable in :file:`conf.py`.
Additionally, this directive supports all the options of the `image`
directive, except for *target* (since plot will add its own target). These
include *alt*, *height*, *width*, *scale*, *align*.
**Configuration options**
.. versionchanged:: 0.45
Prior to v0.45, these directives conflicted with ``matplotlib``. All
directives have been prepended with ``pyvista_``.
The plot directive has the following configuration options:
pyvista_plot_include_source : bool, default: True
Default value for the ``include-source`` directive option.
Default is ``True``.
pyvista_plot_basedir : str
Base directory, to which ``plot::`` file names are relative
to. If ``None`` or unset, file names are relative to the
directory where the file containing the directive is.
pyvista_plot_html_show_formats : bool, default: True
Whether to show links to the files in HTML. Default ``True``.
pyvista_plot_template : str
Provide a customized Jinja2 template for preparing restructured text.
pyvista_plot_setup : str
Python code to be run before every plot directive block.
pyvista_plot_cleanup : str
Python code to be run after every plot directive block.
pyvista_plot_skip : bool, default: False
Default value for the ``skip`` directive option.
pyvista_plot_skip_optional : bool, default: False
Whether to skip execution of ``optional`` directives.
These options can be set by defining global variables of the same name in
:file:`conf.py`.
"""
from __future__ import annotations
import doctest
import os
from os.path import relpath
from pathlib import Path
import re
import shutil
import textwrap
import traceback
from typing import TYPE_CHECKING
from typing import ClassVar
from docutils.parsers.rst import Directive
from docutils.parsers.rst import directives
from docutils.parsers.rst.directives.images import Image
import jinja2 # Sphinx dependency.
# must enable BUILDING_GALLERY to keep windows active
# enable offscreen to hide figures when generating them.
import pyvista
if TYPE_CHECKING:
from collections.abc import Callable
pyvista.BUILDING_GALLERY = True
pyvista.OFF_SCREEN = True
# -----------------------------------------------------------------------------
# Registration hook
# -----------------------------------------------------------------------------
def _option_boolean(arg) -> bool:
if not arg or not arg.strip():
# no argument given, assume used as a flag
return True
elif arg.strip().lower() in ('no', '0', 'false'):
return False
elif arg.strip().lower() in ('yes', '1', 'true'):
return True
else: # pragma: no cover
msg = f'"{arg}" unknown boolean'
raise ValueError(msg)
def _option_context(arg):
if arg is not None: # pragma: no cover
msg = 'No arguments allowed for ``:context:``'
raise ValueError(msg)
def _option_format(arg):
return directives.choice(arg, ('python', 'doctest'))
class PlotDirective(Directive):
"""The ``.. pyvista-plot::`` directive, as documented in the module's docstring."""
has_content = True
required_arguments = 0
optional_arguments = 2
final_argument_whitespace = False
option_spec: ClassVar[dict[str, Callable]] = {
'alt': directives.unchanged,
'height': directives.length_or_unitless,
'width': directives.length_or_percentage_or_unitless,
'scale': directives.nonnegative_int,
'align': Image.align,
'include-source': _option_boolean,
'format': _option_format,
'context': _option_context,
'nofigs': directives.flag,
'encoding': directives.encoding,
'caption': directives.unchanged,
'force_static': directives.flag,
'skip': _option_boolean,
'optional': directives.flag,
}
def run(self):
"""Run the plot directive."""
try:
return run(
self.arguments,
self.content,
self.options,
self.state_machine,
self.state,
self.lineno,
)
except Exception as e: # noqa: BLE001 # pragma: no cover
raise self.error(str(e))
def setup(app):
"""Set up the plot directive."""
setup.app = app
setup.config = app.config
setup.confdir = app.confdir
app.add_directive('pyvista-plot', PlotDirective)
legacy_keys = [
'plot_include_source',
'plot_basedir',
'plot_html_show_formats',
'plot_template',
'plot_setup',
'plot_cleanup',
'plot_skip',
'plot_skip_optional',
]
def raise_on_legacy_config(app, config):
"""Raise a RuntimeError when using legacy configuration parameters.
These parameters conflict with matplotlib's ``plot_directive``.
"""
uses_matplotlib = 'matplotlib.sphinxext.plot_directive' in app.extensions
if not uses_matplotlib: # pragma: no cover
for key in legacy_keys:
if getattr(config, key, None) is not None:
msg = (
f"Sphinx config uses deprecated '{key}' without 'pyvista_' prefix. "
f"Rename it to 'pyvista_{key}"
)
raise RuntimeError(msg)
app.connect('config-inited', raise_on_legacy_config)
app.add_config_value('pyvista_plot_include_source', True, False)
app.add_config_value('pyvista_plot_basedir', None, True)
app.add_config_value('pyvista_plot_html_show_formats', True, True)
app.add_config_value('pyvista_plot_template', None, True)
app.add_config_value('pyvista_plot_setup', None, True)
app.add_config_value('pyvista_plot_cleanup', None, True)
app.add_config_value(name='pyvista_plot_skip', default=False, rebuild='html')
app.add_config_value(name='pyvista_plot_skip_optional', default=False, rebuild='html')
return {
'parallel_read_safe': True,
'parallel_write_safe': True,
'version': pyvista.__version__,
}
# -----------------------------------------------------------------------------
# Doctest handling
# -----------------------------------------------------------------------------
def _contains_doctest(text):
try:
# check if it's valid Python as-is
compile(text, '<string>', 'exec')
except SyntaxError:
pass
else:
return False
r = re.compile(r'^\s*>>>', re.MULTILINE)
m = r.search(text)
return bool(m)
def _contains_pyvista_plot(text) -> bool:
return '.. pyvista-plot::' in text
def _strip_comments(code):
"""Remove comments from a line of python code."""
return re.sub(r'(?m)^ *#.*\n?', '', code)
def _split_code_at_show(text):
"""Split code at plt.show() or plt.plot().
Includes logic to deal with edge cases like:
>>> import pyvista as pv
>>> pv.Sphere().plot(color='blue', cpos='xy')
>>> pv.Sphere().plot(color='red', cpos='xy')
"""
parts = []
is_doctest = _contains_doctest(text)
part = []
within_plot = False
for line in text.split('\n'):
part.append(line)
# check if show(...) or plot(...) is within the line
line_no_comments = _strip_comments(line)
if within_plot: # allow for multi-line plot(...
if line_no_comments.endswith(')'):
parts.append('\n'.join(part))
part = []
within_plot = False
elif _show_or_plot_in_string(line_no_comments):
if line_no_comments.endswith(')'):
parts.append('\n'.join(part))
part = []
else: # allow for multi-line plot(...
within_plot = True
if '\n'.join(part).strip():
parts.append('\n'.join(part))
return is_doctest, parts
def _show_or_plot_in_string(string):
# string contains `.show(`, `.plot(`, or `plot_xyz(` where `xyz` is one
# or more lower-case letters or underscore, e.g. `plot_cell(`, `plot_datasets(`
pattern = r'(?:\.plot\(|\.show\(|(?:[ \t\n.]plot_[a-z_]+?)\()'
return bool(re.search(pattern, string))
# -----------------------------------------------------------------------------
# Template
# -----------------------------------------------------------------------------
TEMPLATE = """
{{ source_code }}
.. only:: html
{% for img in images %}
{% if img.extension == 'vtksz' %}
.. tab-set::
.. tab-item:: Static Scene
.. figure:: {{ build_dir }}/{{ img.stem }}.png
{% for option in options -%}
{{ option }}
{% endfor %}
.. tab-item:: Interactive Scene
.. offlineviewer:: {{ build_dir }}/{{ img.stem }}.vtksz
{{ caption }} {# appropriate leading whitespace added beforehand #}
{% else %}
.. figure:: {{ build_dir }}/{{ img.basename }}
{% for option in options -%}
{{ option }}
{% endfor %}
{{ caption }} {# appropriate leading whitespace added beforehand #}
{% endif %}
{% endfor %}
"""
exception_template = """
.. only:: html
[`source code <%(linkdir)s/%(basename)s.py>`__]
Exception occurred rendering plot.
"""
# the context of the plot for all directives specified with the
# :context: option
plot_context = {}
class ImageFile:
"""Simple representation of an image file path."""
def __init__(self, dirname, basename):
"""Construct ImageFile."""
self.basename = basename
self.dirname = dirname
self.extension = Path(basename).suffix[1:]
@property
def filename(self):
"""Return the filename of this image."""
return str(Path(self.dirname) / self.basename)
@property
def stem(self):
"""Return the basename without the suffix."""
return Path(self.basename).stem
def __repr__(self) -> str: # pragma no cover
return self.filename
class PlotError(RuntimeError):
"""More descriptive plot error."""
def _run_code(*, code, code_path, ns=None, function_name=None): # noqa: ARG001
"""Run a docstring example.
Run the example if it does not contain ``'doctest:+SKIP'``, or a
```pyvista-plot::`` directive. In the later case, the doctest parser will
present the code-block again with the ```pyvista-plot::`` directive
and its options removed.
Import a Python module from a path, and run the function given by
name, if function_name is not None.
"""
# do not execute code containing any SKIP directives
if 'doctest:+SKIP' in code:
return ns
if 'pyvista-plot::' in code:
return ns
try:
if pyvista.PLOT_DIRECTIVE_THEME is not None:
pyvista.set_plot_theme(pyvista.PLOT_DIRECTIVE_THEME) # pragma: no cover
exec(code, ns)
except (Exception, SystemExit) as err: # pragma: no cover
raise PlotError(traceback.format_exc()) from err
return ns
def render_figures(
*,
code,
code_path,
output_dir,
output_base,
context,
function_name,
config,
force_static,
):
"""Run a pyplot script and save the images in *output_dir*.
Save the images under *output_dir* with file names derived from
*output_base*. Closed plotters are ignored if they were never
rendered.
"""
# We skip snippets that contain the ```pyvista-plot::`` directive as part of their code.
# The doctest parser will present the code-block once again with the ```pyvista-plot::``
# directive and its options properly parsed.
if _contains_pyvista_plot(code):
is_doctest = True
code_pieces = [code]
else:
# Try to determine if all images already exist
is_doctest, code_pieces = _split_code_at_show(code)
# Otherwise, we didn't find the files, so build them
results = []
ns = plot_context if context else {}
# Check for setup and teardown code for plots
code_setup = config.pyvista_plot_setup
code_cleanup = config.pyvista_plot_cleanup
if code_setup:
_run_code(code=code_setup, code_path=code_path, ns=ns, function_name=function_name)
try:
for i, code_piece in enumerate(code_pieces):
# generate the plot
_run_code(
code=doctest.script_from_examples(code_piece) if is_doctest else code_piece,
code_path=code_path,
ns=ns,
function_name=function_name,
)
images = []
if (
_show_or_plot_in_string(code_piece)
or '.open_gif' in code_piece
or 'plot=True' in code_piece
):
figures = pyvista.plotting.plotter._ALL_PLOTTERS
for j, (_, plotter) in enumerate(figures.items()):
if plotter._gif_filename is not None:
image_file = ImageFile(output_dir, f'{output_base}_{i:02d}_{j:02d}.gif')
shutil.move(plotter._gif_filename, image_file.filename)
else:
image_file = ImageFile(output_dir, f'{output_base}_{i:02d}_{j:02d}.png')
try:
plotter.screenshot(image_file.filename)
except RuntimeError: # pragma no cover
# ignore closed, unrendered plotters
continue
if force_static or (plotter.last_vtksz is None):
images.append(image_file)
continue
else:
image_file = ImageFile(
output_dir, f'{output_base}_{i:02d}_{j:02d}.vtksz'
)
with Path(image_file.filename).open('wb') as f:
f.write(plotter.last_vtksz)
images.append(image_file)
pyvista.close_all() # close and clear all plotters
results.append((code_piece, images))
finally:
if code_cleanup:
_run_code(code=code_cleanup, code_path=code_path, ns=ns, function_name=function_name)
return results
def run(arguments, content, options, state_machine, state, lineno): # noqa: PLR0917
"""Run the plot directive."""
document = state_machine.document
config = document.settings.env.config
nofigs = 'nofigs' in options
optional = 'optional' in options
force_static = 'force_static' in options
default_fmt = 'png'
options.setdefault('include-source', config.pyvista_plot_include_source)
options.setdefault('skip', config.pyvista_plot_skip)
skip = options['skip'] or (optional and config.pyvista_plot_skip_optional)
keep_context = 'context' in options
_ = None if not keep_context else options['context']
rst_file = document.attributes['source']
rst_dir = str(Path(rst_file).parent)
if len(arguments):
if not config.pyvista_plot_basedir:
source_file_name = str(Path(setup.app.builder.srcdir) / directives.uri(arguments[0]))
else:
source_file_name = str(
Path(setup.confdir) / config.pyvista_plot_basedir / directives.uri(arguments[0]),
)
# If there is content, it will be passed as a caption.
caption = '\n'.join(content)
# Enforce unambiguous use of captions.
if 'caption' in options:
if caption: # pragma: no cover
msg = 'Caption specified in both content and options. Please remove ambiguity.'
raise ValueError(msg)
# Use caption option
caption = options['caption']
# If the optional function name is provided, use it
function_name = arguments[1] if len(arguments) == 2 else None
code = Path(source_file_name).read_text(encoding='utf-8')
output_base = Path(source_file_name).name
else:
source_file_name = rst_file
code = textwrap.dedent('\n'.join(map(str, content)))
# note: this reuses the existing matplotlib plot counter if available
counter = document.attributes.get('_plot_counter', 0) + 1
document.attributes['_plot_counter'] = counter
base = Path(source_file_name).stem
ext = Path(source_file_name).suffix
output_base = f'{base}-{counter}{ext}'
function_name = None
caption = options.get('caption', '')
base = Path(output_base).stem
source_ext = Path(output_base).suffix
if source_ext in ('.py', '.rst', '.txt'):
output_base = base
else:
source_ext = ''
# ensure that LaTeX includegraphics doesn't choke in foo.bar.pdf filenames
output_base = output_base.replace('.', '-')
# is it in doctest format?
is_doctest = _contains_doctest(code)
if 'format' in options:
is_doctest = options['format'] != 'python'
# determine output directory name fragment
source_rel_name = relpath(source_file_name, setup.confdir)
source_rel_dir = str(Path(source_rel_name).parent).lstrip(os.path.sep)
# build_dir: where to place output files (temporarily)
build_dir = str(Path(setup.app.doctreedir).parent / 'plot_directive' / source_rel_dir)
# get rid of .. in paths, also changes pathsep
# see note in Python docs for warning about symbolic links on Windows.
# need to compare source and dest paths at end
build_dir = os.path.normpath(build_dir)
Path(build_dir).mkdir(parents=True, exist_ok=True)
# output_dir: final location in the builder's directory
dest_dir = str((Path(setup.app.builder.outdir) / source_rel_dir).resolve())
Path(dest_dir).mkdir(parents=True, exist_ok=True)
# how to link to files from the RST file
dest_dir_link = os.path.join( # noqa: PTH118
relpath(setup.confdir, rst_dir),
source_rel_dir,
).replace(os.path.sep, '/')
try:
build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/')
except ValueError: # pragma: no cover
# on Windows, relpath raises ValueError when path and start are on
# different mounts/drives
build_dir_link = build_dir
_ = dest_dir_link + '/' + output_base + source_ext
# make figures
errors = []
if skip:
results = [(code, [])]
else:
try:
results = render_figures(
code=code,
code_path=source_file_name,
output_dir=build_dir,
output_base=output_base,
context=keep_context,
function_name=function_name,
config=config,
force_static=force_static,
)
except PlotError as err: # pragma: no cover
reporter = state.memo.reporter
sm = reporter.system_message(
2,
f'Exception occurred in plotting {output_base}\n from {source_file_name}:\n{err}',
line=lineno,
)
results = [(code, [])]
errors.append([sm])
# Properly indent the caption
caption = (
'' if skip else '\n' + '\n'.join(' ' + line.strip() for line in caption.split('\n'))
)
# generate output restructuredtext
total_lines = []
for _, (code_piece, images) in enumerate(results):
if options['include-source']:
if is_doctest:
lines = ['', *code_piece.splitlines()]
else:
lines = [
'.. code-block:: python',
'',
*textwrap.indent(code_piece, ' ').splitlines(),
]
source_code = '\n'.join(lines)
else:
source_code = ''
images_input = [] if nofigs else images
opts = [
f':{key}: {val}'
for key, val in options.items()
if key in ('alt', 'height', 'width', 'scale', 'align')
]
result = jinja2.Template(config.pyvista_plot_template or TEMPLATE).render(
default_fmt=default_fmt,
dest_dir=dest_dir_link,
build_dir=build_dir_link,
source_link=None,
multi_image=len(images_input) > 1,
options=opts,
images=images_input,
source_code=source_code,
html_show_formats=config.pyvista_plot_html_show_formats and len(images_input),
caption=caption,
)
total_lines.extend(result.split('\n'))
total_lines.extend('\n')
if total_lines:
state_machine.insert_input(total_lines, source=source_file_name)
# copy image files to builder's output directory, if necessary
Path(dest_dir).mkdir(parents=True, exist_ok=True)
for _, images in results:
for image in images:
destimg = str(Path(dest_dir) / image.basename)
if image.filename != destimg:
shutil.copyfile(image.filename, destimg)
# copy script (if necessary)
Path(dest_dir, output_base + source_ext).write_text(
doctest.script_from_examples(code)
if source_file_name == rst_file and is_doctest
else code,
encoding='utf-8',
)
return errors
@@ -0,0 +1,107 @@
"""Viewer directive module."""
from __future__ import annotations
import os
from pathlib import Path
import shutil
from docutils import nodes
from docutils.parsers.rst import Directive
from docutils.utils import relative_path # pragma: no cover
from sphinx.util import logging
from trame_vtk.tools.vtksz2html import HTML_VIEWER_PATH
logger = logging.getLogger(__name__)
def is_path_relative_to(path, other):
"""Path.is_relative_to was introduced in Python 3.9 [1].
Provide a replacement that works for all supported versions
[1] https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_relative_to.
"""
return path.is_relative_to(other)
class OfflineViewerDirective(Directive):
required_arguments = 1
optional_arguments = 0
final_argument_whitespace = True
has_content = True
def run(self): # pragma: no cover
source_dir = Path(self.state.document.settings.env.app.srcdir)
output_dir = Path(self.state.document.settings.env.app.outdir)
# _build directory
build_dir = Path(self.state.document.settings.env.app.outdir).parent
# this is the path passed to 'offlineviewer:: <path>` directive
source_file = str(Path(self.state.document.current_source).parent / self.arguments[0])
source_file = Path(source_file).absolute().resolve()
if not Path(source_file).is_file():
logger.warning(f'Source file {source_file} does not exist.')
return []
# copy viewer HTML to _static
static_path = Path(output_dir) / '_static'
static_path.mkdir(exist_ok=True)
if not Path(static_path, Path(HTML_VIEWER_PATH).name).exists():
shutil.copy(HTML_VIEWER_PATH, static_path)
# calculate the scene asset path relative to the build directory and
# recreate the directory structure under output_dir/_images. This
# avoids overriding files with the same name e.g. index-x_yy_zz.vtksz will
# be generated by any index.rst file and we have a number of them.
# Example:
# source_file ${HOME}/pyvista/pyvista/doc/_build/plot_directive/getting-started/index-2_00_00.vtksz # noqa:E501
# dest_partial_path: plot_directive/getting-started
# dest_path: ${HOME}/pyvista/pyvista/doc/_build/html/_images/plot_directive/getting-started/index-2_00_00.vtksz # noqa: E501
if is_path_relative_to(source_file, build_dir):
dest_partial_path = Path(source_file.parent).relative_to(build_dir)
elif is_path_relative_to(source_file, source_dir):
dest_partial_path = Path(source_file.parent).relative_to(source_dir)
else:
logger.warning(
f'Source file {source_file} is not a subpath of either the build directory of the '
f'source directory. Cannot extract base path',
)
return []
dest_path = Path(output_dir).joinpath('_images').joinpath(dest_partial_path)
dest_path.mkdir(parents=True, exist_ok=True)
dest_file = dest_path.joinpath(source_file.name).resolve()
if source_file != dest_file:
try:
shutil.copy(source_file, dest_file)
except Exception as e: # noqa: BLE001
logger.warning(f'Failed to copy file from {source_file} to {dest_file}: {e}')
# Compute the relative path of the current source to the source directory,
# which is the same as the relative path of the '_static' directory to the
# generated HTML file.
relpath_to_source_root = relative_path(self.state.document.current_source, source_dir)
rel_viewer_path = (
Path() / relpath_to_source_root / '_static' / Path(HTML_VIEWER_PATH).name
).as_posix()
rel_asset_path = Path(os.path.relpath(dest_file, static_path)).as_posix()
html = (
f"<iframe src='{rel_viewer_path}?fileURL={rel_asset_path}' "
"width='100%%' height='400px' frameborder='0'></iframe>"
)
raw_node = nodes.raw('', html, format='html')
return [raw_node]
def setup(app):
app.add_directive('offlineviewer', OfflineViewerDirective)
return {
'version': '0.1',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
@@ -0,0 +1,116 @@
"""Jupyter notebook plotting module."""
from __future__ import annotations
import importlib.util
from typing import Literal
from typing import get_args
from typing_extensions import TypeIs
import pyvista
from pyvista.core.errors import PyVistaDeprecationWarning as PyVistaDeprecationWarning
JupyterBackendOptions = Literal['static', 'client', 'server', 'trame', 'html', 'none']
ALLOWED_BACKENDS = get_args(JupyterBackendOptions)
def _is_jupyter_backend(backend: str) -> TypeIs[JupyterBackendOptions]:
"""Return True if backend is allowed jupyter backend."""
return backend in ALLOWED_BACKENDS
def _validate_jupyter_backend(
backend: str | None,
) -> JupyterBackendOptions:
"""Validate that a jupyter backend is valid.
Returns the normalized name of the backend. Raises if the backend is invalid.
"""
# Must be a string
if backend is None:
backend = 'none'
backend = backend.lower()
if not importlib.util.find_spec('IPython'):
msg = 'Install IPython to display with pyvista in a notebook.'
raise ImportError(msg)
if not _is_jupyter_backend(backend):
backend_list_str = ', '.join([f'"{item}"' for item in ALLOWED_BACKENDS])
msg = (
f'Invalid Jupyter notebook plotting backend "{backend}".\n'
f'Use one of the following:\n{backend_list_str}'
)
raise ValueError(msg)
if backend in ['server', 'client', 'trame', 'html']:
try:
from pyvista.trame.jupyter import show_trame as show_trame # noqa: PLC0415
except ImportError: # pragma: no cover
msg = 'Please install trame dependencies: pip install "pyvista[jupyter]"'
raise ImportError(msg)
return backend
def set_jupyter_backend(backend, name=None, **kwargs): # noqa: ARG001
"""Set the plotting backend for a jupyter notebook.
Parameters
----------
backend : str
Jupyter backend to use when plotting. Must be one of the following:
* ``'static'`` : Display a single static image within the
Jupyterlab environment. Still requires that a virtual
framebuffer be set up when displaying on a headless server,
but does not require any additional modules to be installed.
* ``'client'`` : Export/serialize the scene graph to be rendered
with VTK.js client-side through ``trame``. Requires ``trame``
and ``jupyter-server-proxy`` to be installed.
* ``'server'``: Render remotely and stream the resulting VTK
images back to the client using ``trame``. This replaces the
``'ipyvtklink'`` backend with better performance.
Supports the most VTK features, but suffers from minor lag due
to remote rendering. Requires that a virtual framebuffer be set
up when displaying on a headless server. Must have at least ``trame``
and ``jupyter-server-proxy`` installed for cloud/remote Jupyter
instances. This mode is also aliased by ``'trame'``.
* ``'trame'``: The full Trame-based backend that combines both
``'server'`` and ``'client'`` into one backend. This requires a
virtual frame buffer.
* ``'html'`` : Export/serialize the scene graph to be rendered
with the Trame client backend but in a static HTML file.
* ``'none'`` : Do not display any plots within jupyterlab,
instead display using dedicated VTK render windows. This
will generate nothing on headless servers even with a
virtual framebuffer.
name : str, optional
The unique name identifier for the server.
**kwargs : dict, optional
Any additional keyword arguments to pass to the server launch.
Examples
--------
Enable the trame Trame backend.
>>> pv.set_jupyter_backend('trame') # doctest:+SKIP
Just show static images.
>>> pv.set_jupyter_backend('static') # doctest:+SKIP
Disable all plotting within JupyterLab and display using a
standard desktop VTK render window.
>>> pv.set_jupyter_backend(None) # doctest:+SKIP
"""
pyvista.global_theme._jupyter_backend = _validate_jupyter_backend(backend)
@@ -0,0 +1,75 @@
"""Support dynamic or static jupyter notebook plotting.
Includes:
* ``trame``
* ``client``
* ``server``
* ``html``
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import warnings
import numpy as np
if TYPE_CHECKING:
import io
from pathlib import Path
from IPython.lib.display import IFrame
from PIL.Image import Image
from pyvista.jupyter import JupyterBackendOptions
from pyvista.plotting.plotter import Plotter
from pyvista.trame.jupyter import EmbeddableWidget
from pyvista.trame.jupyter import Widget
def handle_plotter(
plotter: Plotter,
backend: JupyterBackendOptions | None = None,
screenshot: str | Path | io.BytesIO | bool | None = None, # noqa: FBT001
**kwargs,
) -> EmbeddableWidget | IFrame | Widget | Image:
"""Show the ``pyvista`` plot in a jupyter environment.
Returns
-------
IPython Widget
IPython widget or image.
"""
if screenshot is False:
screenshot = None
try:
if backend in ['server', 'client', 'trame', 'html']:
from pyvista.trame.jupyter import show_trame # noqa: PLC0415
return show_trame(plotter, mode=backend, **kwargs)
except ImportError as e:
warnings.warn(
f'Failed to use notebook backend: \n\n{e}\n\nFalling back to a static output.',
)
return show_static_image(plotter, screenshot)
def show_static_image(
plotter: Plotter,
screenshot: str | Path | io.BytesIO | bool | None, # noqa: FBT001
) -> Image: # numpydoc ignore=RT01
"""Display a static image to be displayed within a jupyter notebook."""
import PIL.Image # noqa: PLC0415
if plotter.last_image is None:
# Must render here, otherwise plotter will segfault.
plotter.render()
plotter.last_image = plotter.screenshot(screenshot, return_img=True)
assert isinstance(plotter.last_image, np.ndarray)
return PIL.Image.fromarray(plotter.last_image)
@@ -0,0 +1,122 @@
"""Plotting routines."""
from __future__ import annotations
from pyvista import MAX_N_COLOR_BARS as MAX_N_COLOR_BARS
from pyvista._plot import plot as plot
from . import _vtk as _vtk
from ._property import Property as Property
from ._typing import Chart as Chart
from ._typing import ColorLike as ColorLike
from .actor import Actor as Actor
from .actor_properties import ActorProperties as ActorProperties
from .affine_widget import AffineWidget3D as AffineWidget3D
from .axes import Axes as Axes
from .axes_actor import AxesActor as AxesActor
from .axes_assembly import AxesAssembly as AxesAssembly
from .axes_assembly import AxesAssemblySymmetric as AxesAssemblySymmetric
from .axes_assembly import PlanesAssembly as PlanesAssembly
from .camera import Camera as Camera
from .charts import Chart2D as Chart2D
from .charts import ChartBox as ChartBox
from .charts import ChartMPL as ChartMPL
from .charts import ChartPie as ChartPie
from .colors import PARAVIEW_BACKGROUND as PARAVIEW_BACKGROUND
from .colors import Color as Color
from .colors import color_char_to_word as color_char_to_word
from .colors import get_cmap_safe as get_cmap_safe
from .colors import hexcolors as hexcolors
from .composite_mapper import BlockAttributes as BlockAttributes
from .composite_mapper import CompositeAttributes as CompositeAttributes
from .composite_mapper import CompositePolyDataMapper as CompositePolyDataMapper
from .cube_axes_actor import CubeAxesActor as CubeAxesActor
from .errors import InvalidCameraError as InvalidCameraError
from .errors import RenderWindowUnavailable as RenderWindowUnavailable
from .follower import Follower as Follower
from .helpers import plot_arrows as plot_arrows
from .helpers import plot_compare_four as plot_compare_four
from .lights import Light as Light
from .lookup_table import LookupTable as LookupTable
from .mapper import DataSetMapper as DataSetMapper
from .mapper import FixedPointVolumeRayCastMapper as FixedPointVolumeRayCastMapper
from .mapper import GPUVolumeRayCastMapper as GPUVolumeRayCastMapper
from .mapper import OpenGLGPUVolumeRayCastMapper as OpenGLGPUVolumeRayCastMapper
from .mapper import PointGaussianMapper as PointGaussianMapper
from .mapper import SmartVolumeMapper as SmartVolumeMapper
from .mapper import UnstructuredGridVolumeRayCastMapper as UnstructuredGridVolumeRayCastMapper
from .picking import PickingHelper as PickingHelper
from .plotter import _ALL_PLOTTERS as _ALL_PLOTTERS
from .plotter import BasePlotter as BasePlotter
from .plotter import Plotter as Plotter
from .plotter import close_all as close_all
from .prop3d import Prop3D as Prop3D
from .render_window_interactor import RenderWindowInteractor as RenderWindowInteractor
from .render_window_interactor import Timer as Timer
from .renderer import CameraPosition as CameraPosition
from .renderer import Renderer as Renderer
from .renderer import scale_point as scale_point
from .text import CornerAnnotation as CornerAnnotation
from .text import Label as Label
from .text import Text as Text
from .text import TextProperty as TextProperty
from .texture import Texture as Texture
from .texture import image_to_texture as image_to_texture
from .texture import numpy_to_texture as numpy_to_texture
from .themes import DocumentTheme as _GlobalTheme
from .themes import _set_plot_theme_from_env
from .themes import load_theme as load_theme
from .themes import set_plot_theme as set_plot_theme
from .tools import FONTS as FONTS
from .tools import check_math_text_support as check_math_text_support
from .tools import check_matplotlib_vtk_compatibility as check_matplotlib_vtk_compatibility
from .tools import create_axes_marker as create_axes_marker
from .tools import create_axes_orientation_box as create_axes_orientation_box
from .tools import normalize as normalize
from .tools import opacity_transfer_function as opacity_transfer_function
from .tools import parse_font_family as parse_font_family
from .tools import system_supports_plotting as system_supports_plotting
from .utilities import *
from .utilities.sphinx_gallery import _get_sg_image_scraper as _get_sg_image_scraper
from .volume import Volume as Volume
from .volume_property import VolumeProperty as VolumeProperty
from .widgets import WidgetHelper as WidgetHelper
class QtDeprecationError(Exception): # numpydoc ignore=PR01
"""Deprecation Error for features that moved to `pyvistaqt`."""
message = """`{}` has moved to pyvistaqt.
You can install this from PyPI with: `pip install pyvistaqt`
Then import it via: `from pyvistaqt import {}`
`{}` is no longer accessible by `pyvista.{}`
See https://github.com/pyvista/pyvistaqt
"""
def __init__(self, feature_name: str) -> None:
"""Empty init."""
Exception.__init__(self, self.message.format(*[feature_name] * 4))
class BackgroundPlotter: # numpydoc ignore=PR01
"""This class has been moved to pyvistaqt.""" # noqa: D404
def __init__(self, *args, **kwargs) -> None: # noqa: ARG002
"""Empty init."""
msg = 'BackgroundPlotter'
raise QtDeprecationError(msg)
class QtInteractor: # numpydoc ignore=PR01
"""This class has been moved to pyvistaqt.""" # noqa: D404
def __init__(self, *args, **kwargs) -> None: # noqa: ARG002
"""Empty init."""
msg = 'QtInteractor'
raise QtDeprecationError(msg)
global_theme: _GlobalTheme = _GlobalTheme()
# Set preferred plot theme
_set_plot_theme_from_env()
@@ -0,0 +1,296 @@
"""These are private methods we keep out of plotting.py to simplify the module."""
from __future__ import annotations
from typing import TYPE_CHECKING
import warnings
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.arrays import get_array
from pyvista.core.utilities.misc import assert_empty_kwargs
from .colors import Color
from .opts import InterpolationType
from .tools import opacity_transfer_function
if TYPE_CHECKING:
from pyvista.core._typing_core import NumpyArray
@_deprecate_positional_args
def prepare_smooth_shading( # noqa: PLR0917
mesh: pyvista.DataSet, scalars, texture, split_sharp_edges, feature_angle, preference
) -> tuple[pyvista.PolyData, NumpyArray[float]]:
"""Prepare a dataset for smooth shading.
VTK requires datasets with Phong shading to have active normals.
This requires extracting the external surfaces from non-polydata
datasets and computing the point normals.
Parameters
----------
mesh : pyvista.DataSet
Dataset to prepare smooth shading for.
scalars : sequence
Sequence of scalars.
texture : pyvista.Texture or np.ndarray, optional
A texture to apply to the mesh.
split_sharp_edges : bool
Split sharp edges exceeding 30 degrees when plotting with
smooth shading. Control the angle with the optional
keyword argument ``feature_angle``. By default this is
``False``. Note that enabling this will create a copy of
the input mesh within the plotter. See
:ref:`shading_example`.
feature_angle : float
Angle to consider an edge a sharp edge.
preference : str
If the number of points is identical to the number of cells.
Either ``'point'`` or ``'cell'``.
Returns
-------
pyvista.PolyData
Always a surface as we need to compute point normals.
"""
is_polydata = isinstance(mesh, pyvista.PolyData)
indices_array = None
has_scalars = scalars is not None
use_points = False
if has_scalars:
if not isinstance(scalars, np.ndarray):
scalars = np.array(scalars)
if scalars.shape[0] == mesh.n_points and scalars.shape[0] == mesh.n_cells:
use_points = preference == 'point'
else:
use_points = scalars.shape[0] == mesh.n_points
# extract surface if not already a surface
if not is_polydata:
mesh = mesh.extract_surface(
pass_pointid=use_points or texture is not None,
pass_cellid=not use_points,
)
indices_array = 'vtkOriginalPointIds' if use_points else 'vtkOriginalCellIds'
try:
if split_sharp_edges:
mesh = mesh.compute_normals(
cell_normals=False,
split_vertices=True,
feature_angle=feature_angle,
)
if is_polydata:
if has_scalars and use_points:
# we must track the original IDs with our own array from compute_normals
indices_array = 'pyvistaOriginalPointIds'
elif mesh.point_data.active_normals is None:
mesh.compute_normals(cell_normals=False, inplace=True)
except TypeError as e:
if 'Normals cannot be computed' in repr(e):
pass
else:
raise
if has_scalars and indices_array is not None:
ind = mesh[indices_array]
scalars = np.asarray(scalars)[ind]
return mesh, scalars # type: ignore[return-value]
@_deprecate_positional_args
def process_opacity(mesh, opacity, preference, n_colors, scalars, use_transparency): # noqa: PLR0917
"""Process opacity.
This function accepts an opacity string or array and always
returns an array that can be applied to a dataset for plotting.
Parameters
----------
mesh : pyvista.DataSet
Dataset to process the opacity for.
opacity : str, sequence
String or array. If string, can be a ``str`` name of a
predefined mapping such as ``'linear'``, ``'geom'``,
``'sigmoid'``, ``'sigmoid3-10'``, or the key of a cell or
point data array.
preference : str
When ``mesh.n_points == mesh.n_cells``, this parameter
sets how the scalars will be mapped to the mesh. If
``'point'``, causes the scalars will be associated with
the mesh points. Can be either ``'point'`` or
``'cell'``.
n_colors : int
Number of colors to use when displaying the opacity.
scalars : numpy.ndarray
Dataset scalars.
use_transparency : bool
Invert the opacity mappings and make the values correspond
to transparency.
Returns
-------
custom_opac : bool
If using custom opacity.
opacity : numpy.ndarray
Array containing the opacity.
"""
custom_opac = False
if isinstance(opacity, str):
try:
# Get array from mesh
opacity = get_array(mesh, opacity, preference=preference, err=True)
if np.any(opacity > 1):
warnings.warn('Opacity scalars contain values over 1')
if np.any(opacity < 0):
warnings.warn('Opacity scalars contain values less than 0')
custom_opac = True
except KeyError:
# Or get opacity transfer function (e.g. "linear")
opacity = opacity_transfer_function(opacity, n_colors)
else:
if scalars.shape[0] != opacity.shape[0]:
msg = 'Opacity array and scalars array must have the same number of elements.'
raise ValueError(msg)
elif isinstance(opacity, (np.ndarray, list, tuple)):
opacity = np.asanyarray(opacity)
if opacity.shape[0] in [mesh.n_cells, mesh.n_points]:
# User could pass an array of opacities for every point/cell
custom_opac = True
else:
opacity = opacity_transfer_function(opacity, n_colors)
if use_transparency:
if np.max(opacity) <= 1.0:
opacity = 1 - opacity
elif isinstance(opacity, np.ndarray):
opacity = 255 - opacity
return custom_opac, opacity
def _common_arg_parser(
*,
dataset,
theme,
n_colors,
scalar_bar_args,
split_sharp_edges,
show_scalar_bar,
render_points_as_spheres,
smooth_shading,
pbr,
clim,
cmap,
culling,
name,
nan_color,
nan_opacity,
texture,
rgb,
style,
**kwargs,
):
"""Parse arguments in common between add_volume, composite, and mesh."""
# supported aliases
clim = kwargs.pop('rng', clim)
cmap = kwargs.pop('colormap', cmap)
culling = kwargs.pop('backface_culling', culling)
rgb = kwargs.pop('rgba', rgb)
vertex_color = kwargs.pop('vertex_color', theme.edge_color)
vertex_style = kwargs.pop('vertex_style', 'points')
vertex_opacity = kwargs.pop('vertex_opacity', 1.0)
# Support aliases for 'back', 'front', or 'none'. Consider deprecating
if culling is False:
culling = 'none'
elif culling in ['b', 'backface', True]:
culling = 'back'
elif culling in ['f', 'frontface']:
culling = 'front'
if show_scalar_bar is None:
# use theme unless plotting RGB
_default = theme.show_scalar_bar or scalar_bar_args
show_scalar_bar = False if rgb else _default
# Avoid mutating input
scalar_bar_args = {'n_colors': n_colors} if scalar_bar_args is None else scalar_bar_args.copy()
# theme based parameters
if split_sharp_edges is None:
split_sharp_edges = theme.split_sharp_edges
feature_angle = kwargs.pop('feature_angle', theme.sharp_edges_feature_angle)
if render_points_as_spheres is None:
if style == 'points_gaussian':
render_points_as_spheres = False
else:
render_points_as_spheres = theme.render_points_as_spheres
if smooth_shading is None:
smooth_shading = True if pbr else theme.smooth_shading
if name is None:
name = f'{type(dataset).__name__}({dataset.memory_address})'
remove_existing_actor = False
else:
# check if this actor already exists
remove_existing_actor = True
nan_color = Color(nan_color, opacity=nan_opacity, default_color=theme.nan_color)
if texture is False:
texture = None
# allow directly specifying interpolation (potential future feature)
if 'interpolation' in kwargs:
interpolation = kwargs.pop('interpolation') # pragma: no cover:
elif pbr:
interpolation = InterpolationType.PBR
elif smooth_shading:
interpolation = InterpolationType.PHONG
else:
interpolation = theme.lighting_params.interpolation
if 'scalar' in kwargs:
msg = '`scalar` is an invalid keyword argument. Perhaps you mean `scalars` with an s?'
raise TypeError(msg)
assert_empty_kwargs(**kwargs)
return (
scalar_bar_args,
split_sharp_edges,
show_scalar_bar,
feature_angle,
render_points_as_spheres,
smooth_shading,
clim,
cmap,
culling,
name,
nan_color,
texture,
rgb,
interpolation,
remove_existing_actor,
vertex_color,
vertex_style,
vertex_opacity,
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,152 @@
"""Type aliases for type hints."""
from __future__ import annotations
from collections.abc import Sequence
from typing import TYPE_CHECKING
from typing import Literal
from typing import TypedDict
from typing import Union
import matplotlib as mpl
from pyvista.core._typing_core import BoundsTuple as BoundsTuple
from pyvista.core._typing_core import MatrixLike
from pyvista.core._typing_core import Number as Number
from pyvista.core._typing_core import NumpyArray
from pyvista.core._typing_core import VectorLike
from . import _vtk
from .renderer import CameraPosition
if TYPE_CHECKING:
from pyvista.plotting.themes import Theme
from .charts import Chart2D as Chart2D
from .charts import ChartBox as ChartBox
from .charts import ChartMPL as ChartMPL
from .charts import ChartPie as ChartPie
from .colors import _CMCRAMERI_CMAPS_LITERAL
from .colors import _CMOCEAN_CMAPS_LITERAL
from .colors import _COLORCET_CMAPS_LITERAL
from .colors import _MATPLOTLIB_CMAPS_LITERAL
from .colors import Color as Color
NamedColormaps = Union[
'_MATPLOTLIB_CMAPS_LITERAL',
'_CMOCEAN_CMAPS_LITERAL',
'_COLORCET_CMAPS_LITERAL',
'_CMCRAMERI_CMAPS_LITERAL',
]
ColormapOptions = Union[NamedColormaps, list[str], mpl.colors.Colormap]
ColorLike = Union[
tuple[int, int, int],
tuple[int, int, int, int],
tuple[float, float, float],
tuple[float, float, float, float],
Sequence[int],
Sequence[float],
NumpyArray[float],
dict[str, Union[int, float, str]],
str,
'Color',
_vtk.vtkColor3ub,
]
# Overwrite default docstring, as sphinx is not able to capture the docstring
# when it is put beneath the definition somehow?
ColorLike.__doc__ = 'Any object convertible to a :class:`Color`.'
Chart = Union['Chart2D', 'ChartBox', 'ChartPie', 'ChartMPL']
FontFamilyOptions = Literal['courier', 'times', 'arial']
OpacityOptions = Literal[
'linear',
'linear_r',
'geom',
'geom_r',
'sigmoid',
'sigmoid_1',
'sigmoid_2',
'sigmoid_3',
'sigmoid_4',
'sigmoid_5',
'sigmoid_6',
'sigmoid_7',
'sigmoid_8',
'sigmoid_9',
'sigmoid_10',
'sigmoid_15',
'sigmoid_20',
'foreground',
]
CullingOptions = Literal['front', 'back', 'frontface', 'backface', 'f', 'b']
StyleOptions = Literal['surface', 'wireframe', 'points', 'points_gaussian']
LightingOptions = Literal['light kit', 'three lights', 'none']
CameraPositionOptions = Union[
Literal['xy', 'xz', 'yz', 'yx', 'zx', 'zy', 'iso'],
VectorLike[float],
MatrixLike[float],
CameraPosition,
]
class BackfaceArgs(TypedDict, total=False):
theme: Theme
interpolation: Literal['Physically based rendering', 'pbr', 'Phong', 'Gouraud', 'Flat']
color: ColorLike
style: StyleOptions
metallic: float
roughness: float
point_size: float
opacity: float
ambient: float
diffuse: float
specular: float
specular_power: float
show_edges: bool
edge_color: ColorLike
render_points_as_spheres: bool
render_lines_as_tubes: bool
lighting: bool
line_width: float
culling: CullingOptions | bool
edge_opacity: float
class ScalarBarArgs(TypedDict, total=False):
title: str
mapper: _vtk.vtkMapper
n_labels: int
italic: bool
bold: bool
title_font_size: float
label_font_size: float
color: ColorLike
font_family: FontFamilyOptions
shadow: bool
width: float
height: float
position_x: float
position_y: float
vertical: bool
interactive: bool
fmt: str
use_opacity: bool
outline: bool
nan_annotation: bool
below_label: str
above_label: str
background_color: ColorLike
n_colors: int
fill: bool
render: bool
theme: Theme
unconstrained_font_size: bool
class SilhouetteArgs(TypedDict, total=False):
color: ColorLike
line_width: float
opacity: float
feature_angle: float
decimate: float
@@ -0,0 +1,168 @@
"""All imports from VTK (including GL-dependent).
These are the modules within VTK that must be loaded across pyvista's
plotting API. Here, we attempt to import modules using the ``vtkmodules``
package, which lets us only have to import from select modules and not
the entire library.
"""
from __future__ import annotations
from vtkmodules.vtkChartsCore import vtkAxis as vtkAxis
from vtkmodules.vtkChartsCore import vtkChart as vtkChart
from vtkmodules.vtkChartsCore import vtkChartBox as vtkChartBox
from vtkmodules.vtkChartsCore import vtkChartPie as vtkChartPie
from vtkmodules.vtkChartsCore import vtkChartXY as vtkChartXY
from vtkmodules.vtkChartsCore import vtkChartXYZ as vtkChartXYZ
from vtkmodules.vtkChartsCore import vtkPlotArea as vtkPlotArea
from vtkmodules.vtkChartsCore import vtkPlotBar as vtkPlotBar
from vtkmodules.vtkChartsCore import vtkPlotBox as vtkPlotBox
from vtkmodules.vtkChartsCore import vtkPlotLine as vtkPlotLine
from vtkmodules.vtkChartsCore import vtkPlotLine3D as vtkPlotLine3D
from vtkmodules.vtkChartsCore import vtkPlotPie as vtkPlotPie
from vtkmodules.vtkChartsCore import vtkPlotPoints as vtkPlotPoints
from vtkmodules.vtkChartsCore import vtkPlotPoints3D as vtkPlotPoints3D
from vtkmodules.vtkChartsCore import vtkPlotStacked as vtkPlotStacked
from vtkmodules.vtkChartsCore import vtkPlotSurface as vtkPlotSurface
from vtkmodules.vtkCommonColor import vtkColorSeries as vtkColorSeries
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleImage as vtkInteractorStyleImage
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleJoystickActor as vtkInteractorStyleJoystickActor,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleJoystickCamera as vtkInteractorStyleJoystickCamera,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleRubberBand2D as vtkInteractorStyleRubberBand2D,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleRubberBandPick as vtkInteractorStyleRubberBandPick,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleRubberBandZoom as vtkInteractorStyleRubberBandZoom,
)
from vtkmodules.vtkInteractionStyle import vtkInteractorStyleTerrain as vtkInteractorStyleTerrain
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleTrackballActor as vtkInteractorStyleTrackballActor,
)
from vtkmodules.vtkInteractionStyle import (
vtkInteractorStyleTrackballCamera as vtkInteractorStyleTrackballCamera,
)
from vtkmodules.vtkInteractionWidgets import vtkBoxWidget as vtkBoxWidget
from vtkmodules.vtkInteractionWidgets import vtkButtonWidget as vtkButtonWidget
from vtkmodules.vtkInteractionWidgets import (
vtkDistanceRepresentation3D as vtkDistanceRepresentation3D,
)
from vtkmodules.vtkInteractionWidgets import vtkDistanceWidget as vtkDistanceWidget
from vtkmodules.vtkInteractionWidgets import vtkImplicitPlaneWidget as vtkImplicitPlaneWidget
from vtkmodules.vtkInteractionWidgets import vtkLineWidget as vtkLineWidget
from vtkmodules.vtkInteractionWidgets import vtkLogoRepresentation as vtkLogoRepresentation
from vtkmodules.vtkInteractionWidgets import vtkLogoWidget as vtkLogoWidget
from vtkmodules.vtkInteractionWidgets import (
vtkOrientationMarkerWidget as vtkOrientationMarkerWidget,
)
from vtkmodules.vtkInteractionWidgets import vtkPlaneWidget as vtkPlaneWidget
from vtkmodules.vtkInteractionWidgets import (
vtkPointHandleRepresentation3D as vtkPointHandleRepresentation3D,
)
from vtkmodules.vtkInteractionWidgets import vtkResliceCursorPicker as vtkResliceCursorPicker
from vtkmodules.vtkInteractionWidgets import vtkScalarBarWidget as vtkScalarBarWidget
from vtkmodules.vtkInteractionWidgets import vtkSliderRepresentation2D as vtkSliderRepresentation2D
from vtkmodules.vtkInteractionWidgets import vtkSliderWidget as vtkSliderWidget
from vtkmodules.vtkInteractionWidgets import vtkSphereWidget as vtkSphereWidget
from vtkmodules.vtkInteractionWidgets import vtkSplineWidget as vtkSplineWidget
from vtkmodules.vtkInteractionWidgets import (
vtkTexturedButtonRepresentation2D as vtkTexturedButtonRepresentation2D,
)
from vtkmodules.vtkRenderingAnnotation import vtkAnnotatedCubeActor as vtkAnnotatedCubeActor
from vtkmodules.vtkRenderingAnnotation import vtkAxesActor as vtkAxesActor
from vtkmodules.vtkRenderingAnnotation import vtkAxisActor as vtkAxisActor
from vtkmodules.vtkRenderingAnnotation import vtkAxisActor2D as vtkAxisActor2D
from vtkmodules.vtkRenderingAnnotation import vtkCornerAnnotation as vtkCornerAnnotation
from vtkmodules.vtkRenderingAnnotation import vtkCubeAxesActor as vtkCubeAxesActor
from vtkmodules.vtkRenderingAnnotation import vtkLegendBoxActor as vtkLegendBoxActor
from vtkmodules.vtkRenderingAnnotation import vtkLegendScaleActor as vtkLegendScaleActor
from vtkmodules.vtkRenderingAnnotation import vtkScalarBarActor as vtkScalarBarActor
from vtkmodules.vtkRenderingContext2D import vtkBlockItem as vtkBlockItem
from vtkmodules.vtkRenderingContext2D import vtkBrush as vtkBrush
from vtkmodules.vtkRenderingContext2D import vtkContext2D as vtkContext2D
from vtkmodules.vtkRenderingContext2D import vtkContextActor as vtkContextActor
from vtkmodules.vtkRenderingContext2D import vtkContextScene as vtkContextScene
from vtkmodules.vtkRenderingContext2D import vtkImageItem as vtkImageItem
from vtkmodules.vtkRenderingContext2D import vtkPen as vtkPen
try:
from vtkmodules.vtkRenderingCore import vtkHardwarePicker as vtkHardwarePicker
except ImportError: # pragma: no cover
# VTK < 9.2 is missing this class
vtkHardwarePicker = None # type: ignore[assignment, misc] # noqa: N816
from vtkmodules.vtkRenderingCore import VTK_RESOLVE_OFF as VTK_RESOLVE_OFF
from vtkmodules.vtkRenderingCore import VTK_RESOLVE_POLYGON_OFFSET as VTK_RESOLVE_POLYGON_OFFSET
from vtkmodules.vtkRenderingCore import VTK_RESOLVE_SHIFT_ZBUFFER as VTK_RESOLVE_SHIFT_ZBUFFER
from vtkmodules.vtkRenderingCore import vtkAbstractMapper as vtkAbstractMapper
from vtkmodules.vtkRenderingCore import vtkActor as vtkActor
from vtkmodules.vtkRenderingCore import vtkActor2D as vtkActor2D
from vtkmodules.vtkRenderingCore import vtkAreaPicker as vtkAreaPicker
from vtkmodules.vtkRenderingCore import vtkCamera as vtkCamera
from vtkmodules.vtkRenderingCore import vtkCellPicker as vtkCellPicker
from vtkmodules.vtkRenderingCore import vtkColorTransferFunction as vtkColorTransferFunction
from vtkmodules.vtkRenderingCore import (
vtkCompositeDataDisplayAttributes as vtkCompositeDataDisplayAttributes,
)
from vtkmodules.vtkRenderingCore import vtkCompositePolyDataMapper as vtkCompositePolyDataMapper
from vtkmodules.vtkRenderingCore import vtkCoordinate as vtkCoordinate
from vtkmodules.vtkRenderingCore import vtkDataSetMapper as vtkDataSetMapper
from vtkmodules.vtkRenderingCore import vtkFollower as vtkFollower
from vtkmodules.vtkRenderingCore import vtkImageActor as vtkImageActor
from vtkmodules.vtkRenderingCore import vtkInteractorStyle as vtkInteractorStyle
from vtkmodules.vtkRenderingCore import vtkLight as vtkLight
from vtkmodules.vtkRenderingCore import vtkLightActor as vtkLightActor
from vtkmodules.vtkRenderingCore import vtkLightKit as vtkLightKit
from vtkmodules.vtkRenderingCore import vtkMapper as vtkMapper
from vtkmodules.vtkRenderingCore import vtkPointGaussianMapper as vtkPointGaussianMapper
from vtkmodules.vtkRenderingCore import vtkPointPicker as vtkPointPicker
from vtkmodules.vtkRenderingCore import vtkPolyDataMapper as vtkPolyDataMapper
from vtkmodules.vtkRenderingCore import vtkPolyDataMapper2D as vtkPolyDataMapper2D
from vtkmodules.vtkRenderingCore import vtkProp as vtkProp
from vtkmodules.vtkRenderingCore import vtkProp3D as vtkProp3D
from vtkmodules.vtkRenderingCore import vtkPropAssembly as vtkPropAssembly
from vtkmodules.vtkRenderingCore import vtkPropCollection as vtkPropCollection
from vtkmodules.vtkRenderingCore import vtkProperty as vtkProperty
from vtkmodules.vtkRenderingCore import vtkPropPicker as vtkPropPicker
from vtkmodules.vtkRenderingCore import vtkRenderedAreaPicker as vtkRenderedAreaPicker
from vtkmodules.vtkRenderingCore import vtkRenderer as vtkRenderer
from vtkmodules.vtkRenderingCore import vtkRenderWindow as vtkRenderWindow
from vtkmodules.vtkRenderingCore import vtkRenderWindowInteractor as vtkRenderWindowInteractor
from vtkmodules.vtkRenderingCore import vtkScenePicker as vtkScenePicker
from vtkmodules.vtkRenderingCore import vtkSelectVisiblePoints as vtkSelectVisiblePoints
from vtkmodules.vtkRenderingCore import vtkSkybox as vtkSkybox
from vtkmodules.vtkRenderingCore import vtkTextActor as vtkTextActor
from vtkmodules.vtkRenderingCore import vtkTextProperty as vtkTextProperty
from vtkmodules.vtkRenderingCore import vtkTexture as vtkTexture
from vtkmodules.vtkRenderingCore import vtkVolume as vtkVolume
from vtkmodules.vtkRenderingCore import vtkVolumeProperty as vtkVolumeProperty
from vtkmodules.vtkRenderingCore import vtkWindowToImageFilter as vtkWindowToImageFilter
from vtkmodules.vtkRenderingCore import vtkWorldPointPicker as vtkWorldPointPicker
from vtkmodules.vtkRenderingFreeType import (
vtkMathTextFreeTypeTextRenderer as vtkMathTextFreeTypeTextRenderer,
)
from vtkmodules.vtkRenderingFreeType import vtkVectorText as vtkVectorText
from vtkmodules.vtkRenderingLabel import vtkLabelPlacementMapper as vtkLabelPlacementMapper
from vtkmodules.vtkRenderingLabel import vtkPointSetToLabelHierarchy as vtkPointSetToLabelHierarchy
from vtkmodules.vtkRenderingUI import (
vtkGenericRenderWindowInteractor as vtkGenericRenderWindowInteractor,
)
from vtkmodules.vtkRenderingVolume import (
vtkFixedPointVolumeRayCastMapper as vtkFixedPointVolumeRayCastMapper,
)
from vtkmodules.vtkRenderingVolume import vtkGPUVolumeRayCastMapper as vtkGPUVolumeRayCastMapper
from vtkmodules.vtkRenderingVolume import (
vtkUnstructuredGridVolumeRayCastMapper as vtkUnstructuredGridVolumeRayCastMapper,
)
from vtkmodules.vtkRenderingVolume import vtkVolumePicker as vtkVolumePicker
from vtkmodules.vtkViewsContext2D import vtkContextInteractorStyle as vtkContextInteractorStyle
from pyvista.core._vtk_core import *
from ._vtk_gl import *
@@ -0,0 +1,44 @@
"""GL-dependent imports from VTK.
These are the modules within VTK requiring libGL that must be loaded
across pyvista's plotting API. These imports have the potential to
raise an ImportError if the user does not have libGL installed.
ImportError: libGL.so.1: cannot open shared object file: No such file or directory
"""
from __future__ import annotations
import contextlib
try:
# Necessary for displaying charts, otherwise crashes on rendering
from vtkmodules import vtkRenderingContextOpenGL2 as vtkRenderingContextOpenGL2
except ImportError: # pragma: no cover
vtkRenderingContextOpenGL2 = None # type: ignore[assignment] # noqa: N816
from vtkmodules.vtkRenderingOpenGL2 import vtkCameraPass as vtkCameraPass
with contextlib.suppress(ImportError):
from vtkmodules.vtkRenderingOpenGL2 import ( # type: ignore[attr-defined]
vtkCompositePolyDataMapper2 as vtkCompositePolyDataMapper2,
)
from vtkmodules.vtkRenderingOpenGL2 import vtkDepthOfFieldPass as vtkDepthOfFieldPass
from vtkmodules.vtkRenderingOpenGL2 import vtkEDLShading as vtkEDLShading
from vtkmodules.vtkRenderingOpenGL2 import vtkGaussianBlurPass as vtkGaussianBlurPass
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLFXAAPass as vtkOpenGLFXAAPass
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLHardwareSelector as vtkOpenGLHardwareSelector
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLRenderer as vtkOpenGLRenderer
from vtkmodules.vtkRenderingOpenGL2 import vtkOpenGLTexture as vtkOpenGLTexture
from vtkmodules.vtkRenderingOpenGL2 import vtkRenderPassCollection as vtkRenderPassCollection
from vtkmodules.vtkRenderingOpenGL2 import vtkRenderStepsPass as vtkRenderStepsPass
from vtkmodules.vtkRenderingOpenGL2 import vtkSequencePass as vtkSequencePass
from vtkmodules.vtkRenderingOpenGL2 import vtkShadowMapPass as vtkShadowMapPass
from vtkmodules.vtkRenderingOpenGL2 import vtkSSAAPass as vtkSSAAPass
from vtkmodules.vtkRenderingOpenGL2 import vtkSSAOPass as vtkSSAOPass
from vtkmodules.vtkRenderingVolumeOpenGL2 import (
vtkOpenGLGPUVolumeRayCastMapper as vtkOpenGLGPUVolumeRayCastMapper,
)
from vtkmodules.vtkRenderingVolumeOpenGL2 import vtkSmartVolumeMapper as vtkSmartVolumeMapper
@@ -0,0 +1,455 @@
"""Wrap :vtk:`vtkActor` module."""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from . import _vtk
from ._property import Property
from .prop3d import Prop3D
if TYPE_CHECKING:
from typing_extensions import Self
from .mapper import _BaseMapper
class Actor(Prop3D, _vtk.vtkActor):
"""Wrap :vtk:`vtkActor`.
This class represents the geometry & properties in a rendered
scene. Normally, a :class:`pyvista.Actor` is constructed from
:func:`pyvista.Plotter.add_mesh`, but there may be times when it is more
convenient to construct an actor directly from a
:class:`pyvista.DataSetMapper`.
Parameters
----------
mapper : pyvista.DataSetMapper, optional
DataSetMapper.
prop : pyvista.Property, optional
Property of the actor.
name : str, optional
The name of this actor used when tracking on a plotter.
Examples
--------
Create an actor without using :class:`pyvista.Plotter`.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mapper = pv.DataSetMapper(mesh)
>>> actor = pv.Actor(mapper=mapper)
>>> actor
Actor (...)
Center: (0.0, 0.0, 0.0)
Pickable: True
Position: (0.0, 0.0, 0.0)
Scale: (1.0, 1.0, 1.0)
Visible: True
X Bounds -4.993E-01, 4.993E-01
Y Bounds -4.965E-01, 4.965E-01
Z Bounds -5.000E-01, 5.000E-01
User matrix: Identity
Has mapper: True
...
Change the actor properties and plot the actor.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mapper = pv.DataSetMapper(mesh)
>>> actor = pv.Actor(mapper=mapper)
>>> actor.prop.color = 'blue'
>>> actor.plot()
Create an actor using the :class:`pyvista.Plotter` and then change the
visibility of the actor.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> mesh = pv.Sphere()
>>> actor = pl.add_mesh(mesh)
>>> actor.visibility = False
>>> actor.visibility
False
"""
def __init__(self, mapper=None, prop=None, name=None) -> None:
"""Initialize actor."""
super().__init__()
if mapper is not None:
self.mapper = mapper
if prop is None:
self.prop = Property()
else:
self.prop = prop
self._name = name
@property
def mapper(self) -> _BaseMapper: # numpydoc ignore=RT01
"""Return or set the mapper of the actor.
Examples
--------
Create an actor and assign a mapper to it.
>>> import pyvista as pv
>>> dataset = pv.Sphere()
>>> actor = pv.Actor()
>>> actor.mapper = pv.DataSetMapper(dataset)
>>> actor.mapper
DataSetMapper (...)
Scalar visibility: True
Scalar range: (0.0, 1.0)
Interpolate before mapping: True
Scalar map mode: default
Color mode: direct
<BLANKLINE>
Attached dataset:
PolyData (...)
N Cells: 1680
N Points: 842
N Strips: 0
X Bounds: -4.993e-01, 4.993e-01
Y Bounds: -4.965e-01, 4.965e-01
Z Bounds: -5.000e-01, 5.000e-01
N Arrays: 1
"""
return self.GetMapper() # type: ignore[return-value]
@mapper.setter
def mapper(self, obj) -> None:
self.SetMapper(obj)
@property
def prop(self): # numpydoc ignore=RT01
"""Return or set the property of this actor.
Examples
--------
Modify the properties of an actor after adding a dataset to the plotter.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> prop = actor.prop
>>> prop.diffuse = 0.6
>>> pl.show()
"""
return self.GetProperty()
@prop.setter
def prop(self, obj: Property) -> None:
self.SetProperty(obj)
@property
def texture(self): # numpydoc ignore=RT01
"""Return or set the actor texture.
Notes
-----
The mapper dataset must have texture coordinates for the texture to be
used.
Examples
--------
Create an actor and add a texture to it. Note how the
:class:`pyvista.PolyData` has texture coordinates by default.
>>> import pyvista as pv
>>> from pyvista import examples
>>> plane = pv.Plane()
>>> plane.active_texture_coordinates is not None
True
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(plane)
>>> actor.texture = examples.download_masonry_texture()
>>> actor.texture
Texture (...)
Components: 3
Cube Map: False
Dimensions: 256, 256
"""
return self.GetTexture()
@texture.setter
def texture(self, obj) -> None:
self.SetTexture(obj)
@property
def memory_address(self): # numpydoc ignore=RT01
"""Return the memory address of this actor."""
return self.GetAddressAsString('')
@property
def pickable(self) -> bool: # numpydoc ignore=RT01
"""Return or set actor pickability.
Examples
--------
Create an actor using the :class:`pyvista.Plotter` and then make the
actor unpickable.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> actor.pickable = False
>>> actor.pickable
False
"""
return bool(self.GetPickable())
@pickable.setter
def pickable(self, value) -> None:
self.SetPickable(value)
@property
def visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set actor visibility.
See Also
--------
use_bounds
pyvista.Plotter.compute_bounds
Examples
--------
Create an actor using the :class:`pyvista.Plotter` and then change the
visibility of the actor.
>>> import pyvista as pv
>>> from pyvista import examples
>>> mesh = examples.load_airplane()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh)
>>> pl.bounds
BoundsTuple(x_min = 139.06100463867188,
x_max = 1654.9300537109375,
y_min = 32.09429931640625,
y_max = 1319.949951171875,
z_min = -17.741199493408203,
z_max = 282.1300048828125)
>>> actor.visibility = False
>>> pl.bounds
BoundsTuple(x_min = -1.0,
x_max = 1.0,
y_min = -1.0,
y_max = 1.0,
z_min = -1.0,
z_max = 1.0)
"""
return bool(self.GetVisibility())
@visibility.setter
def visibility(self, value: bool) -> None:
self.SetVisibility(value)
@property
def use_bounds(self) -> bool: # numpydoc ignore=RT01
"""Return or set the use of actor's bounds.
.. versionadded:: 0.45
See Also
--------
visibility
pyvista.Plotter.compute_bounds
Examples
--------
Create an actor using the :class:`pyvista.Plotter` and then change the
use of bounds for the actor.
>>> import pyvista as pv
>>> from pyvista import examples
>>> mesh = examples.load_airplane()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh)
>>> pl.bounds
BoundsTuple(x_min = 139.06100463867188,
x_max = 1654.9300537109375,
y_min = 32.09429931640625,
y_max = 1319.949951171875,
z_min = -17.741199493408203,
z_max = 282.1300048828125)
>>> actor.use_bounds = False
>>> pl.bounds
BoundsTuple(x_min = -1.0,
x_max = 1.0,
y_min = -1.0,
y_max = 1.0,
z_min = -1.0,
z_max = 1.0)
Although the actor's bounds are no longer used, the actor remains visible.
>>> actor.visibility
True
"""
return bool(self.GetUseBounds())
@use_bounds.setter
def use_bounds(self, value: bool) -> None:
self.SetUseBounds(value)
def plot(self, **kwargs) -> None:
"""Plot just the actor.
This may be useful when interrogating or debugging individual actors.
Parameters
----------
**kwargs : dict, optional
Optional keyword arguments passed to :func:`pyvista.Plotter.show`.
Examples
--------
Create an actor without the :class:`pyvista.Plotter`, change its
properties, and plot it.
>>> import pyvista as pv
>>> mesh = pv.Sphere()
>>> mapper = pv.DataSetMapper(mesh)
>>> actor = pv.Actor(mapper=mapper)
>>> actor.prop.color = 'red'
>>> actor.prop.show_edges = True
>>> actor.plot()
"""
pl = pyvista.Plotter()
pl.add_actor(self)
pl.show(**kwargs)
@_deprecate_positional_args
def copy(self: Self, deep: bool = True) -> Self: # noqa: FBT001, FBT002
"""Create a copy of this actor.
Parameters
----------
deep : bool, default: True
Create a shallow or deep copy of the actor. A deep copy will have a
new property and mapper, while a shallow copy will use the mapper
and property of this actor.
Returns
-------
Actor
Deep or shallow copy of this actor.
Examples
--------
Create an actor of a cube by adding it to a :class:`~pyvista.Plotter`
and then copy the actor, change the properties, and add it back to the
:class:`~pyvista.Plotter`.
>>> import pyvista as pv
>>> mesh = pv.Cube()
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh, color='b')
>>> new_actor = actor.copy()
>>> new_actor.prop.style = 'wireframe'
>>> new_actor.prop.line_width = 5
>>> new_actor.prop.color = 'r'
>>> new_actor.prop.lighting = False
>>> _ = pl.add_actor(new_actor)
>>> pl.show()
"""
new_actor = type(self)()
if deep:
if self.mapper is not None:
new_actor.mapper = self.mapper.copy()
new_actor.prop = self.prop.copy()
else:
new_actor.ShallowCopy(self)
return new_actor
def __repr__(self):
"""Representation of the actor."""
mat_info = 'Identity' if np.array_equal(self.user_matrix, np.eye(4)) else 'Set'
bnd = self.bounds
attr = [
f'{type(self).__name__} ({hex(id(self))})',
f' Center: {self.center}',
f' Pickable: {self.pickable}',
f' Position: {self.position}',
f' Scale: {self.scale}',
f' Visible: {self.visibility}',
f' X Bounds {bnd[0]:.3E}, {bnd[1]:.3E}',
f' Y Bounds {bnd[2]:.3E}, {bnd[3]:.3E}',
f' Z Bounds {bnd[4]:.3E}, {bnd[5]:.3E}',
f' User matrix: {mat_info}',
f' Has mapper: {self.mapper is not None}',
'',
repr(self.prop),
]
if self.mapper is not None:
attr.append('')
attr.append(repr(self.mapper))
return '\n'.join(attr)
@property
def backface_prop(self) -> pyvista.Property | None: # numpydoc ignore=RT01
"""Return or set the backface property.
By default this property matches the frontface property
:attr:`Actor.prop`. Once accessed or modified, this backface
property becomes independent of the frontface property. In
order to restore the fallback to frontface property, assign
``None`` to the property.
Returns
-------
pyvista.Property
The object describing backfaces.
See Also
--------
:ref:`backface_prop_example`
Examples
--------
Clip a sphere by a plane and color the inside of the clipped sphere
light blue using the ``backface_prop``.
>>> import numpy as np
>>> import pyvista as pv
>>> plane = pv.Plane(i_size=1.5, j_size=1.5)
>>> mesh = pv.Sphere().clip_surface(plane, invert=False)
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(mesh, smooth_shading=True)
>>> actor.backface_prop.color = 'lightblue'
>>> _ = pl.add_mesh(
... plane,
... opacity=0.25,
... show_edges=True,
... color='grey',
... lighting=False,
... )
>>> pl.show()
"""
if self.GetBackfaceProperty() is None:
self.SetBackfaceProperty(self.prop.copy())
return self.GetBackfaceProperty() # type: ignore[return-value]
@backface_prop.setter
def backface_prop(self, value: pyvista.Property) -> None:
self.SetBackfaceProperty(value)
@@ -0,0 +1,152 @@
"""Module containing pyvista implementation of :vtk:`vtkProperty`."""
from __future__ import annotations
from typing import TYPE_CHECKING
from pyvista.core.utilities.misc import _NoNewAttrMixin
from .opts import InterpolationType
from .opts import RepresentationType
if TYPE_CHECKING:
from . import _vtk
class ActorProperties(_NoNewAttrMixin):
"""Properties wrapper for :vtk:`vtkProperty`.
Contains the surface properties of the object.
Parameters
----------
properties : :vtk:`vtkProperty`
VTK properties of the current object.
Examples
--------
Access the properties of the z-axis shaft.
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> z_axes_prop = axes.axes_actor.z_axis_shaft_properties
>>> z_axes_prop.color = (1.0, 1.0, 0.0)
>>> z_axes_prop.opacity = 0.5
>>> axes.axes_actor.shaft_type = axes.axes_actor.ShaftType.CYLINDER
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes.axes_actor)
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.show()
"""
def __init__(self, properties: _vtk.vtkProperty) -> None:
super().__init__()
self.properties = properties
@property
def color(self): # numpydoc ignore=RT01
"""Return or set the color of the actor."""
return self.properties.GetColor()
@color.setter
def color(self, color: tuple[float, float, float]):
self.properties.SetColor(color[0], color[1], color[2])
@property
def metallic(self): # numpydoc ignore=RT01
"""Return or set the metallic coefficient of the surface."""
return self.properties.GetMetallic()
@metallic.setter
def metallic(self, value: float):
self.properties.SetMetallic(value)
@property
def roughness(self): # numpydoc ignore=RT01
"""Return or set the roughness of the surface."""
return self.properties.GetRoughness()
@roughness.setter
def roughness(self, value: float):
self.properties.SetRoughness(value)
@property
def anisotropy(self): # numpydoc ignore=RT01
"""Return or set the anisotropy coefficient."""
return self.properties.GetAnisotropy()
@anisotropy.setter
def anisotropy(self, value: float):
self.properties.SetAnisotropy(value)
@property
def anisotropy_rotation(self): # numpydoc ignore=RT01
"""Return or set the anisotropy rotation coefficient."""
return self.properties.GetAnisotropyRotation()
@anisotropy_rotation.setter
def anisotropy_rotation(self, value: float):
self.properties.SetAnisotropyRotation(value)
@property
def lighting(self): # numpydoc ignore=RT01
"""Return or set the lighting activation flag."""
return self.properties.GetLighting()
@lighting.setter
def lighting(self, flag: bool):
self.properties.SetLighting(flag)
@property
def interpolation_model(self): # numpydoc ignore=RT01
"""Return or set the interpolation model.
Can be any of the options in :class:`pyvista.plotting.opts.InterpolationType` enum.
"""
return InterpolationType.from_any(self.properties.GetInterpolation())
@interpolation_model.setter
def interpolation_model(self, model: InterpolationType):
self.properties.SetInterpolation(model.value)
@property
def index_of_refraction(self): # numpydoc ignore=RT01
"""Return or set the Index Of Refraction of the base layer."""
return self.properties.GetBaseIOR()
@index_of_refraction.setter
def index_of_refraction(self, value: float):
self.properties.SetBaseIOR(value)
@property
def opacity(self): # numpydoc ignore=RT01
"""Return or set the opacity of the actor."""
return self.properties.GetOpacity()
@opacity.setter
def opacity(self, value: float):
self.properties.SetOpacity(value)
@property
def shading(self): # numpydoc ignore=RT01
"""Return or set the flag to activate the shading."""
return self.properties.GetShading()
@shading.setter
def shading(self, is_active: bool):
self.properties.SetShading(is_active)
@property
def representation(self) -> RepresentationType: # numpydoc ignore=RT01
"""Return or set the representation of the actor.
Can be any of the options in :class:`pyvista.plotting.opts.RepresentationType` enum.
"""
return RepresentationType.from_any(self.properties.GetRepresentation())
@representation.setter
def representation(self, value: RepresentationType):
self.properties.SetRepresentation(RepresentationType.from_any(value).value)
@@ -0,0 +1,538 @@
"""Affine widget module."""
from __future__ import annotations
from typing import cast
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.errors import VTKVersionError
from pyvista.core.utilities.misc import _NoNewAttrMixin
from pyvista.core.utilities.misc import try_callback
from . import _vtk
DARK_YELLOW = (0.9647058823529412, 0.7450980392156863, 0)
GLOBAL_AXES = np.eye(3)
def _validate_axes(axes):
"""Validate and normalize input axes.
Axes are expected to follow the right-hand rule (e.g. third axis is the
cross product of the first two.
Parameters
----------
axes : sequence
The axes to be validated and normalized. Should be of shape (3, 3).
Returns
-------
dict
The validated and normalized axes.
"""
axes = np.array(axes)
if axes.shape != (3, 3):
msg = '`axes` must be a (3, 3) array.'
raise ValueError(msg)
axes = axes / np.linalg.norm(axes, axis=1, keepdims=True)
if not np.allclose(np.cross(axes[0], axes[1]), axes[2]):
msg = '`axes` do not follow the right hand rule.'
raise ValueError(msg)
return axes
def _check_callable(func, name='callback'):
"""Check if a variable is callable."""
if func and not callable(func):
msg = f'`{name}` must be a callable, not {type(func)}.'
raise TypeError(msg)
return func
def _make_quarter_arc():
"""Make a quarter circle centered at the origin."""
circ = pyvista.Circle(resolution=100)
circ.faces = np.empty(0, dtype=int)
circ.lines = np.hstack(([26], np.arange(0, 26)))
return circ
def get_angle(v1, v2):
"""Compute the angle between two vectors in degrees.
Parameters
----------
v1 : numpy.ndarray
First input vector.
v2 : numpy.ndarray
Second input vector.
Returns
-------
float
Angle between vectors in degrees.
"""
return np.rad2deg(np.arccos(np.clip(np.dot(v1, v2), -1.0, 1.0)))
@_deprecate_positional_args
def ray_plane_intersection(start_point, direction, plane_point, normal): # noqa: PLR0917
"""Compute the intersection between a ray and a plane.
Parameters
----------
start_point : ndarray
Starting point of the ray.
direction : ndarray
Direction of the ray.
plane_point : ndarray
A point on the plane.
normal : ndarray
Normal to the plane.
Returns
-------
ndarray
Intersection point.
"""
t_value = np.dot(normal, (plane_point - start_point)) / np.dot(normal, direction)
return start_point + t_value * direction
class AffineWidget3D(_NoNewAttrMixin):
"""3D affine transform widget.
This widget allows interactive transformations including translation and
rotation using the left mouse button.
Parameters
----------
plotter : pyvista.Plotter
The plotter object.
actor : pyvista.Actor
The actor to which the widget is attached to.
origin : sequence[float], optional
Origin of the widget. Default is the center of the main actor.
start : bool, default: True
If True, start the widget immediately.
scale : float, default: 0.15
Scale factor for the widget relative to the length of the actor.
line_radius : float, default: 0.02
Relative radius of the lines composing the widget.
always_visible : bool, default: True
Make the widget always visible. Setting this to ``False`` will cause
the widget geometry to be hidden by other actors in the plotter.
axes_colors : tuple[ColorLike], optional
Uses the theme by default. Configure the individual axis colors by
modifying either the theme with ``pyvista.global_theme.axes.x_color =
<COLOR>`` or setting this with a ``tuple`` as in ``('r', 'g', 'b')``.
axes : numpy.ndarray, optional
``(3, 3)`` Numpy array defining the X, Y, and Z axes. By default this
matches the default coordinate system.
release_callback : callable, optional
Call this method when releasing the left mouse button. It is passed the
``user_matrix`` of the actor.
interact_callback : callable, optional
Call this method when moving the mouse with the left mouse button
pressed down and a valid movement actor selected. It is passed the
``user_matrix`` of the actor.
Notes
-----
After interacting with the actor, the transform will be stored within
:attr:`pyvista.Prop3D.user_matrix` but will not be applied to the
dataset. Use this matrix in conjunction with
:func:`pyvista.DataObjectFilters.transform` to transform the dataset.
Requires VTK >= v9.2
Examples
--------
Create the affine widget outside of the plotter and add it.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> actor = pl.add_mesh(pv.Sphere())
>>> widget = pv.AffineWidget3D(pl, actor)
>>> pl.show()
Access the transform from the actor.
>>> actor.user_matrix
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
"""
@_deprecate_positional_args(allowed=['plotter', 'actor'])
def __init__( # noqa: PLR0917
self,
plotter,
actor,
origin=None,
start: bool = True, # noqa: FBT001, FBT002
scale=0.15,
line_radius=0.02,
always_visible: bool = True, # noqa: FBT001, FBT002
axes_colors=None,
axes=None,
release_callback=None,
interact_callback=None,
):
"""Initialize the widget."""
# needs VTK v9.2.0 due to the hardware picker
if pyvista.vtk_version_info < (9, 2):
msg = 'AfflineWidget3D requires VTK v9.2.0 or newer.'
raise VTKVersionError(msg)
self._axes = np.eye(4)
self._axes_inv = np.eye(4)
self._pl = plotter
self._main_actor = actor
self._selected_actor: pyvista.Actor | None = None
self._init_position = None
self._mouse_move_observer: int | None = None
self._left_press_observer: int | None = None
self._left_release_observer: int | None = None
if self._main_actor.user_matrix is None:
self._main_actor.user_matrix = np.eye(4)
self._cached_matrix = self._main_actor.user_matrix
self._arrows = [] # type: ignore[var-annotated]
self._circles = [] # type: ignore[var-annotated]
self._pressing_down = False
origin = origin or actor.center
self._origin = np.array(origin)
if axes_colors is None:
axes_colors = (
pyvista.global_theme.axes.x_color,
pyvista.global_theme.axes.y_color,
pyvista.global_theme.axes.z_color,
)
self._axes_colors = axes_colors
self._circ = _make_quarter_arc()
self._actor_length = self._main_actor.GetLength()
self._line_radius = line_radius
self._user_interact_callback = _check_callable(interact_callback)
self._user_release_callback = _check_callable(release_callback)
self._init_actors(scale, always_visible)
# axes must be set after initializing actors
if axes is not None:
try:
_validate_axes(axes)
except ValueError:
for actor_ in self._arrows + self._circles:
self._pl.remove_actor(actor_)
raise
self.axes = axes
if start:
self.enable()
def _init_actors(self, scale, always_visible):
"""Initialize the widget's actors."""
for ii, color in enumerate(self._axes_colors):
arrow = pyvista.Arrow(
start=(0, 0, 0),
direction=GLOBAL_AXES[ii],
scale=self._actor_length * scale * 1.15,
tip_radius=0.05,
shaft_radius=self._line_radius,
)
self._arrows.append(
self._pl.add_mesh(arrow, color=color, lighting=False, render=False)
)
axis_circ = self._circ.copy()
if ii == 0:
axis_circ = axis_circ.rotate_y(-90)
elif ii == 1:
axis_circ = axis_circ.rotate_x(90)
axis_circ.points *= self._main_actor.GetLength() * (scale * 1.6)
# axis_circ.points += self._origin
axis_circ = axis_circ.tube(
radius=self._line_radius * self._actor_length * scale,
absolute=True,
radius_factor=1.0,
)
self._circles.append(
self._pl.add_mesh(
axis_circ,
color=color,
lighting=False,
render_lines_as_tubes=True,
render=False,
),
)
# update origin and assign a default user_matrix
for actor in self._arrows + self._circles:
matrix = np.eye(4)
matrix[:3, -1] = self._origin
actor.user_matrix = matrix
if always_visible:
for actor in self._arrows + self._circles:
actor.mapper.SetResolveCoincidentTopologyToPolygonOffset()
actor.mapper.SetRelativeCoincidentTopologyPolygonOffsetParameters(0, -20000)
def _get_world_coord_rot(self, interactor):
"""Get the world coordinates given an interactor.
Unlike ``_get_world_coord_trans``, these coordinates are physically
accurate, but sensitive to the position of the camera. Rotation is zoom
independent.
"""
x, y = interactor.GetEventPosition()
coordinate = _vtk.vtkCoordinate()
coordinate.SetCoordinateSystemToDisplay()
coordinate.SetValue(x, y, 0)
ren = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer()
point = np.array(coordinate.GetComputedWorldValue(ren))
if self._selected_actor:
index = self._circles.index(self._selected_actor)
to_widget = np.array(ren.camera.position - self._origin)
point = ray_plane_intersection(
start_point=point,
direction=to_widget,
plane_point=self._origin,
normal=self.axes[index],
)
return point
def _get_world_coord_trans(self, interactor):
"""Get the world coordinates given an interactor.
This uses a modified scaled approach to get the world coordinates that
are not physically accurate, but ignores zoom and works for
translation.
"""
x, y = interactor.GetEventPosition()
ren = interactor.GetRenderWindow().GetRenderers().GetFirstRenderer()
# Get normalized view coordinates (-1, 1)
width, height = ren.GetSize()
ndc_x = 2 * (x / width) - 1
ndc_y = 2 * (y / height) - 1
ndc_z = 1
# convert camera coordinates to world coordinates
camera = ren.GetActiveCamera()
projection_matrix = pyvista.array_from_vtkmatrix(
camera.GetProjectionTransformMatrix(ren.GetTiledAspectRatio(), 0, 1),
)
inverse_projection_matrix = np.linalg.inv(projection_matrix)
camera_coords = np.dot(inverse_projection_matrix, [ndc_x, ndc_y, ndc_z, 1])
modelview_matrix = pyvista.array_from_vtkmatrix(camera.GetModelViewTransformMatrix())
inverse_modelview_matrix = np.linalg.inv(modelview_matrix)
world_coords = np.dot(inverse_modelview_matrix, camera_coords)
# Scale by twice actor length (experimentally determined for good UX)
return world_coords[:3] * self._actor_length * 2
def _move_callback(self, interactor, _event):
"""Process actions for the move mouse event."""
click_x, click_y = interactor.GetEventPosition()
click_z = 0
picker = interactor.GetPicker()
renderer = interactor.GetInteractorStyle()._parent()._plotter.iren.get_poked_renderer()
picker.Pick(click_x, click_y, click_z, renderer)
actor = picker.GetActor()
if self._pressing_down:
if self._selected_actor in self._arrows:
current_pos = self._get_world_coord_trans(interactor)
index = self._arrows.index(self._selected_actor)
diff = current_pos - self._init_position
trans_matrix = np.eye(4)
trans_matrix[:3, -1] = self.axes[index] * np.dot(diff, self.axes[index])
matrix = trans_matrix @ self._cached_matrix
elif self._selected_actor in self._circles:
current_pos = self._get_world_coord_rot(interactor)
index = self._circles.index(self._selected_actor)
vec_current = current_pos - self._origin
vec_init = self._init_position - self._origin
normal = self.axes[index]
vec_current = vec_current - np.dot(vec_current, normal) * normal
vec_init = vec_init - np.dot(vec_init, normal) * normal
vec_current /= np.linalg.norm(vec_current)
vec_init /= np.linalg.norm(vec_init)
angle = get_angle(vec_init, vec_current)
cross = np.cross(vec_init, vec_current)
if cross[index] < 0:
angle = -angle
trans = _vtk.vtkTransform()
trans.Translate(self._origin) # type: ignore[call-overload]
trans.RotateWXYZ(
angle,
self._axes[index][0],
self._axes[index][1],
self._axes[index][2],
)
trans.Translate(-self._origin) # type: ignore[call-overload]
trans.Update()
rot_matrix = pyvista.array_from_vtkmatrix(trans.GetMatrix())
matrix = rot_matrix @ self._cached_matrix
if self._user_interact_callback:
try_callback(self._user_interact_callback, self._main_actor.user_matrix)
self._main_actor.user_matrix = matrix
elif self._selected_actor and self._selected_actor is not actor:
# Return the color of the currently selected actor to normal and
# deselect it
if self._selected_actor in self._arrows:
index = self._arrows.index(self._selected_actor)
elif self._selected_actor in self._circles:
index = self._circles.index(self._selected_actor)
self._selected_actor.prop.color = self._axes_colors[index]
self._selected_actor = None
# Highlight the actor if there is no selected actor
if actor and not self._selected_actor:
if actor in self._arrows:
index = self._arrows.index(actor)
self._arrows[index].prop.color = DARK_YELLOW
actor.prop.color = DARK_YELLOW
self._selected_actor = actor
elif actor in self._circles:
index = self._circles.index(actor)
self._circles[index].prop.color = DARK_YELLOW
actor.prop.color = DARK_YELLOW
self._selected_actor = actor
self._pl.render()
def _press_callback(self, interactor, _event):
"""Process actions for the mouse button press event."""
if self._selected_actor:
self._pl.enable_trackball_actor_style()
self._pressing_down = True
if self._selected_actor in self._circles:
self._init_position = self._get_world_coord_rot(interactor)
else:
self._init_position = self._get_world_coord_trans(interactor)
def _release_callback(self, _interactor, _event):
"""Process actions for the mouse button release event."""
self._pl.enable_trackball_style()
self._pressing_down = False
self._cached_matrix = self._main_actor.user_matrix
if self._user_release_callback:
try_callback(self._user_release_callback, self._main_actor.user_matrix)
def _reset(self):
"""Reset the actor and cached transform."""
self._main_actor.user_matrix = np.eye(4)
self._cached_matrix = np.eye(4)
@property
def axes(self):
"""Return or set the axes of the widget.
The axes will be checked for orthogonality. Non-orthogonal axes will
raise a ``ValueError``
Returns
-------
numpy.ndarray
``(3, 3)`` array of axes.
"""
return self._axes[:3, :3]
@axes.setter
def axes(self, axes):
mat = np.eye(4)
mat[:3, :3] = _validate_axes(axes)
mat[:3, -1] = self.origin
self._axes = mat
self._axes_inv = np.linalg.inv(self._axes) # type: ignore[assignment]
for actor in self._arrows + self._circles:
matrix = actor.user_matrix
# Be sure to use the inverse here
matrix[:3, :3] = self._axes_inv[:3, :3]
actor.user_matrix = matrix
@property
def origin(self) -> tuple[float, float, float]:
"""Origin of the widget.
This is where the origin of the widget will be located and where the
actor will be rotated about.
Returns
-------
tuple
Widget origin.
"""
return cast('tuple[float, float, float]', tuple(self._origin))
@origin.setter
def origin(self, value):
value = np.array(value)
diff = value - self._origin
for actor in self._circles + self._arrows:
if actor.user_matrix is None:
actor.user_matrix = np.eye(4)
matrix = actor.user_matrix
matrix[:3, -1] += diff
actor.user_matrix = matrix
self._origin = value
def enable(self):
"""Enable the widget."""
if not self._pl._picker_in_use:
self._pl.enable_mesh_picking(show_message=False, show=False, picker='hardware')
self._mouse_move_observer = self._pl.iren.add_observer(
'MouseMoveEvent',
self._move_callback,
)
self._left_press_observer = self._pl.iren.add_observer(
'LeftButtonPressEvent',
self._press_callback,
interactor_style_fallback=False,
)
self._left_release_observer = self._pl.iren.add_observer(
'LeftButtonReleaseEvent',
self._release_callback,
interactor_style_fallback=False,
)
def disable(self):
"""Disable the widget."""
self._pl.disable_picking()
if self._mouse_move_observer:
self._pl.iren.remove_observer(self._mouse_move_observer)
if self._left_press_observer:
self._pl.iren.remove_observer(self._left_press_observer)
if self._left_release_observer:
self._pl.iren.remove_observer(self._left_release_observer)
def remove(self):
"""Disable and delete all actors of this widget."""
self.disable()
for actor in self._circles + self._arrows:
self._pl.remove_actor(actor)
self._circles = []
self._arrows = []
@@ -0,0 +1,136 @@
"""Module containing pyvista implementation of :vtk:`vtkAxes`."""
from __future__ import annotations
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .actor import Actor
from .axes_actor import AxesActor
class Axes(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkAxes):
"""PyVista wrapper for the VTK Axes class.
Parameters
----------
show_actor : bool, optional
Hide or show the actor of these axes. Default ``False``.
actor_scale : float, optional
Scale the size of the axes actor. Default ``1``.
line_width : float, optional
Width of the axes lines. Default ``1``.
symmetric : bool, optional
If true, the axis continue to negative values.
Examples
--------
Create an instance of axes at the pyvista module level.
>>> import pyvista as pv
>>> axes = pv.Axes()
"""
@_deprecate_positional_args
def __init__( # noqa: PLR0917
self,
show_actor: bool = False, # noqa: FBT001, FBT002
actor_scale=1,
line_width=1.0,
symmetric: bool = False, # noqa: FBT001, FBT002
): # numpydoc ignore=PR01,RT01
"""Initialize a new axes descriptor."""
super().__init__()
self.SetSymmetric(symmetric)
# Add the axes mapper
self.mapper = _vtk.vtkPolyDataMapper()
self.mapper.SetInputConnection(self.GetOutputPort())
# Add the axes actor
self.actor = Actor(mapper=self.mapper)
self.axes_actor = AxesActor()
self.actor.visibility = show_actor
self.actor.scale = actor_scale
self.actor.prop.line_width = line_width
@property
def origin(self): # numpydoc ignore=RT01
"""Return or set th origin of the axes in world coordinates.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.origin
(0.0, 0.0, 0.0)
Set the origin of the camera.
>>> axes.origin = (2.0, 1.0, 1.0)
>>> axes.origin
(2.0, 1.0, 1.0)
"""
return self.GetOrigin()
@origin.setter
def origin(self, value):
self.SetOrigin(value)
def show_actor(self):
"""Show an actor of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.show_actor()
"""
self.actor.visibility = True
def hide_actor(self):
"""Hide an actor of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.hide_actor()
"""
self.actor.visibility = False
def show_symmetric(self):
"""Show symmetric of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.show_symmetric()
"""
self.SymmetricOn()
def hide_symmetric(self):
"""Hide symmetric of axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.hide_symmetric()
"""
self.SymmetricOff()
def __del__(self):
"""Clean the attributes of the class."""
self.axes_actor = None # type: ignore[assignment]
self.actor = None # type: ignore[assignment]
self.mapper = None # type: ignore[assignment]
@@ -0,0 +1,665 @@
"""Axes actor module."""
from __future__ import annotations
from collections.abc import Iterable
from enum import Enum
import warnings
import pyvista
from pyvista.core._typing_core import BoundsTuple
from pyvista.core.errors import PyVistaDeprecationWarning
from pyvista.core.utilities.misc import _BoundsSizeMixin
from pyvista.core.utilities.misc import _NameMixin
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .actor_properties import ActorProperties
class AxesActor(
_NoNewAttrMixin, _NameMixin, _BoundsSizeMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkAxesActor
):
"""Axes actor wrapper for :vtk:`vtkAxesActor`.
Hybrid 2D/3D actor used to represent 3D axes in a scene. The user
can define the geometry to use for the shaft or the tip, and the
user can set the text for the three axes. To see full customization
options, refer to :vtk:`vtkAxesActor`.
See Also
--------
:class:`~pyvista.AxesAssembly`
:ref:`axes_objects_example`
Example showing different axes objects.
Examples
--------
Customize the axis shaft color and shape.
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.z_axis_shaft_properties.color = (0.0, 1.0, 1.0)
>>> axes.axes_actor.shaft_type = axes.axes_actor.ShaftType.CYLINDER
>>> pl = pv.Plotter()
>>> _ = pl.add_actor(axes.axes_actor)
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.show()
Or you can use this as a custom orientation widget with
:func:`add_orientation_widget() <pyvista.Renderer.add_orientation_widget>`:
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes_actor = axes.axes_actor
>>> axes.axes_actor.shaft_type = 0
>>> axes_actor.x_axis_shaft_properties.color = (1.0, 1.0, 1.0)
>>> axes_actor.y_axis_shaft_properties.color = (1.0, 1.0, 1.0)
>>> axes_actor.z_axis_shaft_properties.color = (1.0, 1.0, 1.0)
>>> axes_actor.x_label = 'U'
>>> axes_actor.y_label = 'V'
>>> axes_actor.z_label = 'W'
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Cone())
>>> _ = pl.add_orientation_widget(
... axes_actor,
... viewport=(0, 0, 0.5, 0.5),
... )
>>> pl.show()
"""
class ShaftType(Enum):
"""Types of shaft shapes available."""
CYLINDER = 0
LINE = 1
class TipType(Enum):
"""Types of tip shapes available."""
CONE = 0
SPHERE = 1
def __init__(self):
"""Initialize actor."""
super().__init__()
self.x_axis_shaft_properties.color = pyvista.global_theme.axes.x_color.float_rgb
self.x_axis_tip_properties.color = pyvista.global_theme.axes.x_color.float_rgb
self.x_axis_shaft_properties.opacity = pyvista.global_theme.axes.x_color.float_rgba[3]
self.x_axis_tip_properties.opacity = pyvista.global_theme.axes.x_color.float_rgba[3]
self.x_axis_shaft_properties.lighting = pyvista.global_theme.lighting
self.y_axis_shaft_properties.color = pyvista.global_theme.axes.y_color.float_rgb
self.y_axis_tip_properties.color = pyvista.global_theme.axes.y_color.float_rgb
self.y_axis_shaft_properties.opacity = pyvista.global_theme.axes.y_color.float_rgba[3]
self.y_axis_tip_properties.opacity = pyvista.global_theme.axes.y_color.float_rgba[3]
self.y_axis_shaft_properties.lighting = pyvista.global_theme.lighting
self.z_axis_shaft_properties.color = pyvista.global_theme.axes.z_color.float_rgb
self.z_axis_tip_properties.color = pyvista.global_theme.axes.z_color.float_rgb
self.z_axis_shaft_properties.opacity = pyvista.global_theme.axes.z_color.float_rgba[3]
self.z_axis_tip_properties.opacity = pyvista.global_theme.axes.z_color.float_rgba[3]
self.z_axis_shaft_properties.lighting = pyvista.global_theme.lighting
@property
def bounds(self) -> BoundsTuple:
"""Return the bounding box of this.
Returns
-------
BoundsTuple
Bounding box.
The form is: ``(x_min, x_max, y_min, y_max, z_min, z_max)``.
"""
return BoundsTuple(*self.GetBounds())
@property
def center(self) -> tuple[float, float, float]:
"""Return the center.
Returns
-------
tuple[float, float, float]
Center of axes actor.
"""
return self.GetCenter()
@property
def visibility(self) -> bool: # numpydoc ignore=RT01
"""Return or set AxesActor visibility.
Examples
--------
Create an Axes object and then access the
visibility attribute of its AxesActor.
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.visibility
True
"""
return bool(self.GetVisibility())
@visibility.setter
def visibility(self, value: bool):
self.SetVisibility(value)
@property
def total_length(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the length of all axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.total_length
(1.0, 1.0, 1.0)
>>> axes.axes_actor.total_length = 1.2
>>> axes.axes_actor.total_length
(1.2, 1.2, 1.2)
>>> axes.axes_actor.total_length = (1.0, 0.9, 0.5)
>>> axes.axes_actor.total_length
(1.0, 0.9, 0.5)
"""
return self.GetTotalLength()
@total_length.setter
def total_length(self, length):
if isinstance(length, Iterable):
self.SetTotalLength(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetTotalLength(length, length, length)
@property
def shaft_length(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the length of the axes shaft.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.shaft_length
(0.8, 0.8, 0.8)
>>> axes.axes_actor.shaft_length = 0.7
>>> axes.axes_actor.shaft_length
(0.7, 0.7, 0.7)
>>> axes.axes_actor.shaft_length = (1.0, 0.9, 0.5)
>>> axes.axes_actor.shaft_length
(1.0, 0.9, 0.5)
"""
return self.GetNormalizedShaftLength()
@shaft_length.setter
def shaft_length(self, length):
if isinstance(length, Iterable):
self.SetNormalizedShaftLength(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetNormalizedShaftLength(length, length, length)
@property
def tip_length(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Return or set the length of the tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.tip_length
(0.2, 0.2, 0.2)
>>> axes.axes_actor.tip_length = 0.3
>>> axes.axes_actor.tip_length
(0.3, 0.3, 0.3)
>>> axes.axes_actor.tip_length = (0.1, 0.4, 0.2)
>>> axes.axes_actor.tip_length
(0.1, 0.4, 0.2)
"""
return self.GetNormalizedTipLength()
@tip_length.setter
def tip_length(self, length):
if isinstance(length, Iterable):
self.SetNormalizedTipLength(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetNormalizedTipLength(length, length, length)
@property
def label_position(self) -> tuple[float, float, float]: # numpydoc ignore=RT01
"""Position of the label along the axes.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.label_position
(1.0, 1.0, 1.0)
>>> axes.axes_actor.label_position = 0.3
>>> axes.axes_actor.label_position
(0.3, 0.3, 0.3)
>>> axes.axes_actor.label_position = (0.1, 0.4, 0.2)
>>> axes.axes_actor.label_position
(0.1, 0.4, 0.2)
"""
return self.GetNormalizedLabelPosition()
@label_position.setter
def label_position(self, length):
if isinstance(length, Iterable):
self.SetNormalizedLabelPosition(length[0], length[1], length[2]) # type: ignore[index]
else:
self.SetNormalizedLabelPosition(length, length, length)
@property
def cone_resolution(self) -> int: # numpydoc ignore=RT01
"""Return or set the resolution of the cone tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cone_resolution
16
>>> axes.axes_actor.cone_resolution = 24
>>> axes.axes_actor.cone_resolution
24
"""
return self.GetConeResolution()
@cone_resolution.setter
def cone_resolution(self, res: int):
self.SetConeResolution(res)
@property
def sphere_resolution(self) -> int: # numpydoc ignore=RT01
"""Return or set the resolution of the spherical tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.sphere_resolution
16
>>> axes.axes_actor.sphere_resolution = 24
>>> axes.axes_actor.sphere_resolution
24
"""
return self.GetSphereResolution()
@sphere_resolution.setter
def sphere_resolution(self, res: int):
self.SetSphereResolution(res)
@property
def cylinder_resolution(self) -> int: # numpydoc ignore=RT01
"""Return or set the resolution of the shaft cylinder.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cylinder_resolution
16
>>> axes.axes_actor.cylinder_resolution = 24
>>> axes.axes_actor.cylinder_resolution
24
"""
return self.GetCylinderResolution()
@cylinder_resolution.setter
def cylinder_resolution(self, res: int):
self.SetCylinderResolution(res)
@property
def cone_radius(self) -> float: # numpydoc ignore=RT01
"""Return or set the radius of the cone tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cone_radius
0.4
>>> axes.axes_actor.cone_radius = 0.8
>>> axes.axes_actor.cone_radius
0.8
"""
return self.GetConeRadius()
@cone_radius.setter
def cone_radius(self, rad: float):
self.SetConeRadius(rad)
@property
def sphere_radius(self) -> float: # numpydoc ignore=RT01
"""Return or set the radius of the spherical tip.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.sphere_radius
0.4
>>> axes.axes_actor.sphere_radius = 0.8
>>> axes.axes_actor.sphere_radius
0.8
"""
return self.GetSphereRadius()
@sphere_radius.setter
def sphere_radius(self, rad: float):
self.SetSphereRadius(rad)
@property
def cylinder_radius(self) -> float: # numpydoc ignore=RT01
"""Return or set the radius of the shaft cylinder.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.cylinder_radius
0.05
>>> axes.axes_actor.cylinder_radius = 0.03
>>> axes.axes_actor.cylinder_radius
0.03
"""
return self.GetCylinderRadius()
@cylinder_radius.setter
def cylinder_radius(self, rad: float):
self.SetCylinderRadius(rad)
@property
def shaft_type(self) -> ShaftType: # numpydoc ignore=RT01
"""Return or set the shaft type.
Can be either a cylinder(0) or a line(1).
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.shaft_type = axes.axes_actor.ShaftType.LINE
>>> axes.axes_actor.shaft_type
<ShaftType.LINE: 1>
"""
return AxesActor.ShaftType(self.GetShaftType())
@shaft_type.setter
def shaft_type(self, shaft_type: ShaftType | int):
shaft_type = AxesActor.ShaftType(shaft_type)
if shaft_type == AxesActor.ShaftType.CYLINDER:
self.SetShaftTypeToCylinder()
elif shaft_type == AxesActor.ShaftType.LINE:
self.SetShaftTypeToLine()
@property
def tip_type(self) -> TipType: # numpydoc ignore=RT01
"""Return or set the shaft type.
Can be either a cone(0) or a sphere(1).
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.tip_type = axes.axes_actor.TipType.SPHERE
>>> axes.axes_actor.tip_type
<TipType.SPHERE: 1>
"""
return AxesActor.TipType(self.GetTipType())
@tip_type.setter
def tip_type(self, tip_type: TipType | int):
tip_type = AxesActor.TipType(tip_type)
if tip_type == AxesActor.TipType.CONE:
self.SetTipTypeToCone()
elif tip_type == AxesActor.TipType.SPHERE:
self.SetTipTypeToSphere()
@property
def labels(self) -> tuple[str, str, str]: # numpydoc ignore=RT01
"""Return or set the axes labels.
This property may be used as an alternative to using :attr:`~x_axis_label`,
:attr:`~y_axis_label`, and :attr:`~z_axis_label` separately.
.. versionadded:: 0.44.0
Examples
--------
>>> import pyvista as pv
>>> axes_actor = pv.AxesActor()
>>> axes_actor.labels = ['X Axis', 'Y Axis', 'Z Axis']
>>> axes_actor.labels
('X Axis', 'Y Axis', 'Z Axis')
"""
return self.x_label, self.y_label, self.z_label
@labels.setter
def labels(self, labels: list[str] | tuple[str]):
if not isinstance(labels, (list, tuple)):
msg = f'Labels must be a list or tuple. Got {labels} instead.' # type: ignore[unreachable]
raise TypeError(msg)
if len(labels) != 3:
msg = f'Labels must be a list or tuple with three items. Got {labels} instead.'
raise ValueError(msg)
self.x_label = labels[0]
self.y_label = labels[1]
self.z_label = labels[2]
@property
def x_axis_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the x-axis.
.. deprecated:: 0.44.0
This parameter is deprecated. Use :attr:`x_label` instead.
"""
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `x_axis_label` is deprecated. Use `x_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
return self.GetXAxisLabelText() # pragma: no cover
@x_axis_label.setter
def x_axis_label(self, label: str):
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `x_axis_label` is deprecated. Use `x_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
self.SetXAxisLabelText(label) # pragma: no cover
@property
def x_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the x-axis.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.x_label = 'This axis'
>>> axes.axes_actor.x_label
'This axis'
"""
return self.GetXAxisLabelText()
@x_label.setter
def x_label(self, label: str):
self.SetXAxisLabelText(label)
@property
def y_axis_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the y-axis.
.. deprecated:: 0.44.0
This parameter is deprecated. Use :attr:`y_label` instead.
"""
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `y_axis_label` is deprecated. Use `y_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
return self.GetYAxisLabelText() # pragma: no cover
@y_axis_label.setter
def y_axis_label(self, label: str):
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `y_axis_label` is deprecated. Use `y_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
self.SetYAxisLabelText(label) # pragma: no cover
@property
def y_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the y-axis.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.y_label = 'This axis'
>>> axes.axes_actor.y_label
'This axis'
"""
return self.GetYAxisLabelText()
@y_label.setter
def y_label(self, label: str):
self.SetYAxisLabelText(label)
@property
def z_axis_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the z-axis.
.. deprecated:: 0.44.0
This parameter is deprecated. Use :attr:`z_label` instead.
"""
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `z_axis_label` is deprecated. Use `z_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
return self.GetZAxisLabelText() # pragma: no cover
@z_axis_label.setter
def z_axis_label(self, label: str):
# deprecated 0.44.0, convert to error in 0.46.0, remove 0.47.0
warnings.warn(
'Use of `z_axis_label` is deprecated. Use `z_label` instead.',
PyVistaDeprecationWarning,
)
if pyvista._version.version_info >= (0, 47): # pragma: no cover
msg = 'Remove this deprecated property'
raise RuntimeError(msg)
self.SetZAxisLabelText(label) # pragma: no cover
@property
def z_label(self) -> str: # numpydoc ignore=RT01
"""Return or set the label for the z-axis.
Examples
--------
>>> import pyvista as pv
>>> axes = pv.Axes()
>>> axes.axes_actor.z_label = 'This axis'
>>> axes.axes_actor.z_label
'This axis'
"""
return self.GetZAxisLabelText()
@z_label.setter
def z_label(self, label: str):
self.SetZAxisLabelText(label)
@property
def x_axis_shaft_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the x-axis shaft."""
return ActorProperties(self.GetXAxisShaftProperty())
@property
def y_axis_shaft_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the y-axis shaft."""
return ActorProperties(self.GetYAxisShaftProperty())
@property
def z_axis_shaft_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the z-axis shaft."""
return ActorProperties(self.GetZAxisShaftProperty())
@property
def x_axis_tip_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the x-axis tip."""
return ActorProperties(self.GetXAxisTipProperty())
@x_axis_tip_properties.setter
def x_axis_tip_properties(self, properties: ActorProperties):
self.x_axis_tip_properties = properties
@property
def y_axis_tip_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the y-axis tip."""
return ActorProperties(self.GetYAxisTipProperty())
@y_axis_tip_properties.setter
def y_axis_tip_properties(self, properties: ActorProperties):
self.y_axis_tip_properties = properties
@property
def z_axis_tip_properties(self): # numpydoc ignore=RT01
"""Return or set the properties of the z-axis tip."""
return ActorProperties(self.GetZAxisTipProperty())
@z_axis_tip_properties.setter
def z_axis_tip_properties(self, properties: ActorProperties):
self.z_axis_tip_properties = properties
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,100 @@
"""Contains the BackgroundRenderer class."""
from __future__ import annotations
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from .renderer import Renderer
class BackgroundRenderer(Renderer):
"""BackgroundRenderer for visualizing a background image.
Parameters
----------
parent : Renderer
The parent renderer for the background renderer.
image_path : str
Path to the image to use as a background.
scale : float, default: 1
Scaling factor for the background image.
view_port : tuple[float], optional
Viewport for the background renderer.
"""
@_deprecate_positional_args(allowed=['parent', 'image_path'])
def __init__( # noqa: PLR0917
self, parent, image_path, scale=1, view_port=None
):
"""Initialize BackgroundRenderer with an image."""
# avoiding circular import
from . import _vtk # noqa: PLC0415
# read the image first as we don't need to create a render if
# the image path is invalid
image_data = pyvista.read(image_path)
super().__init__(parent, border=False)
self.SetLayer(0)
self.InteractiveOff()
self.SetBackground(self.parent.renderer.GetBackground())
self._scale = scale
self._modified_observer = None
self._prior_window_size = None
if view_port is not None:
self.viewport = view_port
# create image actor
image_actor = _vtk.vtkImageActor()
image_actor.SetInputData(image_data)
self.add_actor(image_actor, name='background')
self.camera.enable_parallel_projection()
self.reset_camera() # necessary to get first render
self.resize()
def resize(self, *args): # noqa: ARG002
"""Resize a background renderer.
Parameters
----------
*args : tuple
Ignored arguments.
"""
if self.parent is None: # when deleted
return
if self.parent.render_window is None: # BasePlotter
return
if self._prior_window_size != self.parent.window_size:
self._prior_window_size = self.parent.window_size
actor = self._actors['background']
image_data = actor.GetInput()
origin = image_data.GetOrigin()
extent = image_data.GetExtent()
spacing = image_data.GetSpacing()
xc = origin[0] + 0.5 * (extent[0] + extent[1]) * spacing[0]
yc = origin[1] + 0.5 * (extent[2] + extent[3]) * spacing[1]
yd = (extent[3] - extent[2] + 1) * spacing[1]
dist = self.camera.distance
# make the longest dimensions match the plotting window
img_dim = np.array(image_data.dimensions[:2])
self.camera._focus = np.array([xc, yc, 0.0])
self.camera.position = np.array([xc, yc, dist])
ratio = img_dim / np.array(self.parent.window_size)
scale_value = 1
if ratio.max() > 1:
# images are not scaled if larger than the window
scale_value = ratio.max()
if self._scale is not None:
scale_value /= self._scale
self.camera.parallel_scale = 0.5 * yd / self._scale
@@ -0,0 +1,912 @@
"""Module containing pyvista implementation of :vtk:`vtkCamera`."""
from __future__ import annotations
from pathlib import Path
from weakref import proxy
import xml.dom.minidom as md
from xml.etree import ElementTree as ET
import numpy as np
import pyvista
from pyvista._deprecate_positional_args import _deprecate_positional_args
from pyvista.core.utilities.misc import _NoNewAttrMixin
from . import _vtk
from .helpers import view_vectors
class Camera(_NoNewAttrMixin, _vtk.DisableVtkSnakeCase, _vtk.vtkCamera):
"""PyVista wrapper for the VTK Camera class.
Parameters
----------
renderer : pyvista.Renderer, optional
Renderer to attach the camera to.
Examples
--------
Create a camera at the pyvista module level.
>>> import pyvista as pv
>>> camera = pv.Camera()
Access the active camera of a plotter and get the position of the
camera.
>>> pl = pv.Plotter()
>>> pl.camera.position
(1.0, 1.0, 1.0)
"""
def __init__(self, renderer=None):
"""Initialize a new camera descriptor."""
self._parallel_projection = False
self._elevation = 0.0
self._azimuth = 0.0
self._is_set = False
self._focus = None # Used by BackgroundRenderer
if renderer:
if not isinstance(renderer, pyvista.Renderer):
msg = 'Camera only accepts a pyvista.Renderer or None as the ``renderer`` argument'
raise TypeError(msg)
self._renderer = proxy(renderer)
else:
self._renderer = None # type: ignore[assignment]
def __eq__(self, other) -> bool:
"""Compare whether the relevant attributes of two cameras are equal."""
# attributes which are native python types and thus implement __eq__
native_attrs = [
'position',
'focal_point',
'parallel_projection',
'distance',
'thickness',
'parallel_scale',
'clipping_range',
'view_angle',
'roll',
]
for attr in native_attrs:
if getattr(self, attr) != getattr(other, attr):
return False
this_trans = self.model_transform_matrix
that_trans = other.model_transform_matrix
trans_count = sum(1 for trans in [this_trans, that_trans] if trans is not None)
if trans_count == 1:
# either but not both are None
return False
return not (trans_count == 2 and not np.array_equal(this_trans, that_trans))
__hash__ = None # type: ignore[assignment] # https://github.com/pyvista/pyvista/pull/7671
def __repr__(self):
"""Print a repr specifying the id of the camera and its camera type."""
repr_str = f'{self.__class__.__name__} ({hex(id(self))})'
repr_str += f'\n Position: {self.position}'
repr_str += f'\n Focal Point: {self.focal_point}'
repr_str += f'\n Parallel Projection: {self.parallel_projection}'
repr_str += f'\n Distance: {self.distance}'
repr_str += f'\n Thickness: {self.thickness}'
repr_str += f'\n Parallel Scale: {self.parallel_scale}'
repr_str += f'\n Clipping Range: {self.clipping_range}'
repr_str += f'\n View Angle: {self.view_angle}'
repr_str += f'\n Roll: {self.roll}'
return repr_str
def __str__(self):
"""Return the object string representation."""
return self.__repr__()
def __del__(self):
"""Delete the camera."""
self.RemoveAllObservers()
@property
def is_set(self) -> bool: # numpydoc ignore=RT01
"""Get or set whether this camera has been configured."""
return self._is_set
@is_set.setter
def is_set(self, value: bool):
self._is_set = bool(value)
@classmethod
def from_paraview_pvcc(cls, filename: str | Path) -> Camera:
"""Load a Paraview camera file (.pvcc extension).
Returns a pyvista.Camera object for which attributes has been read
from the filename argument.
Parameters
----------
filename : str or pathlib.Path
Path to Paraview camera file (.pvcc).
Returns
-------
pyvista.Camera
Camera from the camera file.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera = pv.Camera.from_paraview_pvcc('camera.pvcc') # doctest:+SKIP
>>> pl.camera.position
(1.0, 1.0, 1.0)
"""
to_find = {
'CameraPosition': ('position', float),
'CameraFocalPoint': ('focal_point', float),
'CameraViewAngle': ('view_angle', float),
'CameraViewUp': ('up', float),
'CameraParallelProjection': ('parallel_projection', int),
'CameraParallelScale': ('parallel_scale', float),
}
camera = cls()
tree = ET.parse(filename)
root = tree.getroot()[0]
for element in root:
attrib = element.attrib
attrib_name = attrib['name']
if attrib_name in to_find:
name, typ = to_find[attrib_name]
nelems = int(attrib['number_of_elements'])
# Set the camera attributes
if nelems == 3:
values = [typ(e.attrib['value']) for e in element]
setattr(camera, name, values)
elif nelems == 1:
# Special case for bool since bool("0") returns True.
# So first convert to int from `to_find` and then apply bool
if 'name' in element[-1].attrib and element[-1].attrib['name'] == 'bool':
val = bool(typ(element[0].attrib['value']))
else:
val = typ(element[0].attrib['value'])
setattr(camera, name, val)
camera.is_set = True
return camera
def to_paraview_pvcc(self, filename: str | Path):
"""Write the camera parameters to a Paraview camera file (.pvcc extension).
Parameters
----------
filename : str or pathlib.Path
Path to Paraview camera file (.pvcc).
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.to_paraview_pvcc('camera.pvcc') # doctest:+SKIP
"""
root = ET.Element('PVCameraConfiguration')
root.attrib['description'] = 'ParaView camera configuration'
root.attrib['version'] = '1.0'
dico = dict(group='views', type='RenderView', id='0', servers='21')
proxy = ET.SubElement(root, 'Proxy', dico)
# Add tuples
to_find = {
'CameraPosition': 'position',
'CameraFocalPoint': 'focal_point',
'CameraViewUp': 'up',
}
for name, attr in to_find.items():
e = ET.SubElement(
proxy,
'Property',
dict(name=name, id=f'0.{name}', number_of_elements='3'),
)
for i in range(3):
tmp = ET.Element('Element')
tmp.attrib['index'] = str(i)
tmp.attrib['value'] = str(getattr(self, attr)[i])
e.append(tmp)
# Add single values
to_find = {
'CameraViewAngle': 'view_angle',
'CameraParallelScale': 'parallel_scale',
'CameraParallelProjection': 'parallel_projection',
}
for name, attr in to_find.items():
e = ET.SubElement(
proxy,
'Property',
dict(name=name, id=f'0.{name}', number_of_elements='1'),
)
tmp = ET.Element('Element')
tmp.attrib['index'] = '0'
val = getattr(self, attr)
if not isinstance(val, bool):
tmp.attrib['value'] = str(val)
e.append(tmp)
else:
tmp.attrib['value'] = '1' if val else '0'
e.append(tmp)
e.append(ET.Element('Domain', dict(name='bool', id=f'0.{name}.bool')))
xmlstr = ET.tostring(root).decode()
newxml = md.parseString(xmlstr)
with Path(filename).open('w') as outfile:
outfile.write(newxml.toprettyxml(indent='\t', newl='\n'))
@property
def position(self): # numpydoc ignore=RT01
"""Return or set the position of the camera in world coordinates.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.position
(1.0, 1.0, 1.0)
>>> pl.camera.position = (2.0, 1.0, 1.0)
>>> pl.camera.position
(2.0, 1.0, 1.0)
"""
return self.GetPosition()
@position.setter
def position(self, value):
self.SetPosition(value)
self._elevation = 0.0
self._azimuth = 0.0
if self._renderer: # type: ignore[truthy-bool]
self.reset_clipping_range()
self.is_set = True
def reset_clipping_range(self):
"""Reset the camera clipping range based on the bounds of the visible actors.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.clipping_range = (1, 2)
>>> pl.camera.reset_clipping_range() # doctest:+SKIP
(0.0039213485598532955, 3.9213485598532953)
"""
if self._renderer is None:
msg = 'Camera is must be associated with a renderer to reset its clipping range.' # type: ignore[unreachable]
raise AttributeError(msg)
self._renderer.reset_camera_clipping_range()
@property
def focal_point(self): # numpydoc ignore=RT01
"""Location of the camera's focus in world coordinates.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.focal_point
(0.0, 0.0, 0.0)
>>> pl.camera.focal_point = (2.0, 0.0, 0.0)
>>> pl.camera.focal_point
(2.0, 0.0, 0.0)
"""
return self.GetFocalPoint()
@focal_point.setter
def focal_point(self, point):
self.SetFocalPoint(point)
self.is_set = True
@property
def model_transform_matrix(self): # numpydoc ignore=RT01
"""Return or set the camera's model transformation matrix.
Examples
--------
>>> import pyvista as pv
>>> import numpy as np
>>> pl = pv.Plotter()
>>> pl.camera.model_transform_matrix
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.]])
>>> pl.camera.model_transform_matrix = np.array(
... [
... [1.0, 0.0, 0.0, 0.0],
... [0.0, 1.0, 0.0, 0.0],
... [0.0, 0.0, 1.0, 0.0],
... [0.0, 0.0, 0.0, 0.5],
... ]
... )
>>>
array([[1., 0., 0., 0.],
[0., 1., 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 0.5]])
"""
vtk_matrix = self.GetModelTransformMatrix()
matrix = np.empty((4, 4))
vtk_matrix.DeepCopy(matrix.ravel(), vtk_matrix)
return matrix
@model_transform_matrix.setter
def model_transform_matrix(self, matrix):
vtk_matrix = _vtk.vtkMatrix4x4()
vtk_matrix.DeepCopy(matrix.ravel())
self.SetModelTransformMatrix(vtk_matrix)
@property
def distance(self): # numpydoc ignore=RT01
"""Return or set the distance of the focal point from the camera.
Notes
-----
Setting the distance keeps the camera fixed and moves the focal point.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.distance
1.73205
>>> pl.camera.distance = 2.0
>>> pl.camera.distance
2.0
"""
return self.GetDistance()
@distance.setter
def distance(self, distance):
self.SetDistance(distance)
self.is_set = True
@property
def thickness(self): # numpydoc ignore=RT01
"""Return or set the distance between clipping planes.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.thickness
1000.0
>>> pl.camera.thickness = 100
>>> pl.camera.thickness
100.0
"""
return self.GetThickness()
@thickness.setter
def thickness(self, length):
self.SetThickness(length)
@property
def parallel_scale(self): # numpydoc ignore=RT01
"""Return or set the scaling used for a parallel projection.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.parallel_scale
1.0
>>> pl.camera.parallel_scale = 2.0
>>> pl.camera.parallel_scale
2.0
"""
return self.GetParallelScale()
@parallel_scale.setter
def parallel_scale(self, scale):
self.SetParallelScale(scale)
def zoom(self, value):
"""Set the zoom of the camera.
In perspective mode, decrease the view angle by the specified
factor.
In parallel mode, decrease the parallel scale by the specified
factor. A value greater than 1 is a zoom-in, a value less than
1 is a zoom-out.
Parameters
----------
value : float or str
Zoom of the camera. If a float, must be greater than 0. Otherwise,
if a string, must be ``"tight"``. If tight, the plot will be zoomed
such that the actors fill the entire viewport.
Examples
--------
Show the Default zoom.
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.zoom(1.0)
>>> pl.show()
Show 2x zoom.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.zoom(2.0)
>>> pl.show()
Zoom so the actor fills the entire render window.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(pv.Sphere())
>>> pl.camera.zoom('tight')
>>> pl.show()
"""
if isinstance(value, str):
if value != 'tight':
msg = 'If a string, ``zoom`` can only be "tight"'
raise ValueError(msg)
self.tight()
return
self.Zoom(value)
self.is_set = True
@property
def up(self): # numpydoc ignore=RT01
"""Return or set the "up" of the camera.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.up
(0.0, 0.0, 1.0)
>>> pl.camera.up = (0.410018, 0.217989, 0.885644)
>>> pl.camera.up
(0.410018, 0.217989, 0.885644)
"""
return self.GetViewUp()
@up.setter
def up(self, vector):
self.SetViewUp(vector)
self.is_set = True
def enable_parallel_projection(self):
"""Enable parallel projection.
The camera will have a parallel projection. Parallel
projection is often useful when viewing images or 2D datasets,
but will look odd when viewing 3D datasets.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import demos
>>> pl = pv.demos.orientation_plotter()
>>> pl.enable_parallel_projection()
>>> pl.show()
"""
self._parallel_projection = True
self.SetParallelProjection(True)
def disable_parallel_projection(self):
"""Disable the use of parallel projection.
This is default behavior.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import demos
>>> pl = pv.demos.orientation_plotter()
>>> pl.disable_parallel_projection()
>>> pl.show()
"""
self._parallel_projection = False
self.SetParallelProjection(False)
@property
def parallel_projection(self): # numpydoc ignore=RT01
"""Return the state of the parallel projection.
Examples
--------
>>> import pyvista as pv
>>> from pyvista import demos
>>> pl = pv.Plotter()
>>> pl.disable_parallel_projection()
>>> pl.parallel_projection
False
"""
return self._parallel_projection
@parallel_projection.setter
def parallel_projection(self, state):
if state:
self.enable_parallel_projection()
else:
self.disable_parallel_projection()
@property
def clipping_range(self): # numpydoc ignore=RT01
"""Return or set the location of the clipping planes.
Clipping planes are the near and far clipping planes along
the direction of projection.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.clipping_range
(0.01, 1000.01)
>>> pl.camera.clipping_range = (1, 10)
>>> pl.camera.clipping_range
(1.0, 10.0)
"""
return self.GetClippingRange()
@clipping_range.setter
def clipping_range(self, points):
if points[0] > points[1]:
msg = 'Near point must be lower than the far point.'
raise ValueError(msg)
self.SetClippingRange(points[0], points[1])
@property
def view_angle(self): # numpydoc ignore=RT01
"""Return or set the camera view angle.
Examples
--------
>>> import pyvista as pv
>>> plotter = pv.Plotter()
>>> plotter.camera.view_angle
30.0
>>> plotter.camera.view_angle = 60.0
>>> plotter.camera.view_angle
60.0
"""
return self.GetViewAngle()
@view_angle.setter
def view_angle(self, value):
self.SetViewAngle(value)
@property
def direction(self): # numpydoc ignore=RT01
"""Vector from the camera position to the focal point.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.direction # doctest:+SKIP
(-0.5773502691896257, -0.5773502691896257, -0.5773502691896257)
"""
return self.GetDirectionOfProjection()
def view_frustum(self, aspect=1.0):
"""Get the view frustum.
Parameters
----------
aspect : float, default: 1.0
The aspect of the viewport to compute the planes.
Returns
-------
pyvista.PolyData
View frustum.
Examples
--------
>>> import pyvista as pv
>>> plotter = pv.Plotter()
>>> frustum = plotter.camera.view_frustum(1.0)
>>> frustum.n_points
8
>>> frustum.n_cells
6
"""
frustum_planes = [0] * 24
self.GetFrustumPlanes(aspect, frustum_planes) # type: ignore[arg-type]
planes = _vtk.vtkPlanes()
planes.SetFrustumPlanes(frustum_planes) # type: ignore[arg-type]
frustum_source = _vtk.vtkFrustumSource()
frustum_source.ShowLinesOff()
frustum_source.SetPlanes(planes)
frustum_source.Update()
return pyvista.wrap(frustum_source.GetOutput())
@property
def roll(self): # numpydoc ignore=RT01
"""Return or set the roll of the camera about the direction of projection.
This will spin the camera about its axis.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.roll
-120.00000000000001
>>> pl.camera.roll = 45.0
>>> pl.camera.roll
45.0
"""
return self.GetRoll()
@roll.setter
def roll(self, angle):
self.SetRoll(angle)
self.is_set = True
@property
def elevation(self): # numpydoc ignore=RT01
"""Return or set the vertical rotation of the scene.
Rotate the camera about the cross product of the negative of
the direction of projection and the view up vector, using the
focal point as the center of rotation.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.elevation
0.0
>>> pl.camera.elevation = 45.0
>>> pl.camera.elevation
45.0
"""
return self._elevation
@elevation.setter
def elevation(self, angle):
if self._elevation:
self.Elevation(-self._elevation)
self._elevation = angle
self.Elevation(angle)
self.is_set = True
@property
def azimuth(self): # numpydoc ignore=RT01
"""Return or set the azimuth of the camera.
Rotate the camera about the view up vector centered at the
focal point. Note that the view up vector is whatever was set
via SetViewUp, and is not necessarily perpendicular to the
direction of projection.
Examples
--------
>>> import pyvista as pv
>>> pl = pv.Plotter()
>>> pl.camera.azimuth
0.0
>>> pl.camera.azimuth = 45.0
>>> pl.camera.azimuth
45.0
"""
return self._azimuth
@azimuth.setter
def azimuth(self, angle):
if self._azimuth:
self.Azimuth(-self._azimuth)
self._azimuth = angle
self.Azimuth(angle)
self.is_set = True
def copy(self):
"""Return a deep copy of the camera.
Returns
-------
pyvista.Camera
Deep copy of the camera.
Examples
--------
Create a camera and check that it shares a transformation
matrix with its shallow copy.
>>> import pyvista as pv
>>> import numpy as np
>>> camera = pv.Camera()
>>> camera.model_transform_matrix = np.array(
... [
... [1.0, 0.0, 0.0, 0.0],
... [0.0, 1.0, 0.0, 0.0],
... [0.0, 0.0, 1.0, 0.0],
... [0.0, 0.0, 0.0, 1.0],
... ]
... )
>>> copied_camera = camera.copy()
>>> copied_camera == camera
True
>>> camera.model_transform_matrix = np.array(
... [
... [1.0, 0.0, 0.0, 0.0],
... [0.0, 1.0, 0.0, 0.0],
... [0.0, 0.0, 1.0, 0.0],
... [0.0, 0.0, 0.0, 0.5],
... ]
... )
>>> copied_camera == camera
False
"""
immutable_attrs = [
'position',
'focal_point',
'model_transform_matrix',
'distance',
'thickness',
'parallel_scale',
'up',
'clipping_range',
'view_angle',
'roll',
'parallel_projection',
'is_set',
]
new_camera = Camera()
for attr in immutable_attrs:
value = getattr(self, attr)
setattr(new_camera, attr, value)
return new_camera
@_deprecate_positional_args
def tight( # noqa: PLR0917
self,
padding=0.0,
adjust_render_window: bool = True, # noqa: FBT001, FBT002
view='xy',
negative: bool = False, # noqa: FBT001, FBT002
):
"""Adjust the camera position so that the actors fill the entire renderer.
The camera view direction is reoriented to be normal to the ``view``
plane. When ``negative=False``, The first letter of ``view`` refers
to the axis that points to the right. The second letter of ``view``
refers to axis that points up. When ``negative=True``, the first
letter refers to the axis that points left. The up direction is
unchanged.
Parallel projection is enabled when using this function.
Parameters
----------
padding : float, default: 0.0
Additional padding around the actor(s). This is effectively a zoom,
where a value of 0.01 results in a zoom out of 1%.
adjust_render_window : bool, default: True
Adjust the size of the render window as to match the dimensions of
the visible actors.
view : {'xy', 'yx', 'xz', 'zx', 'yz', 'zy'}, default: 'xy'
Plane to which the view is oriented.
negative : bool, default: False
Whether to view in opposite direction.
Notes
-----
This resets the view direction to look at a plane with parallel projection.
Examples
--------
Display the puppy image with a tight view.
>>> import pyvista as pv
>>> from pyvista import examples
>>> puppy = examples.download_puppy()
>>> pl = pv.Plotter(border=True, border_width=5)
>>> _ = pl.add_mesh(puppy, rgb=True)
>>> pl.camera.tight()
>>> pl.show()
Set the background to blue use a 5% padding around the image.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(puppy, rgb=True)
>>> pl.background_color = 'b'
>>> pl.camera.tight(padding=0.05)
>>> pl.show()
"""
# Inspired by vedo resetCamera. Thanks @marcomusy.
x0, x1, y0, y1, z0, z1 = self._renderer.bounds
self.enable_parallel_projection()
self._renderer.ComputeAspect()
aspect = self._renderer.GetAspect()
position0 = np.array([x0, y0, z0])
position1 = np.array([x1, y1, z1])
objects_size = position1 - position0
position = position0 + objects_size / 2
direction, viewup = view_vectors(view, negative=negative)
horizontal = np.cross(direction, viewup)
vert_dist = abs(objects_size @ viewup)
horiz_dist = abs(objects_size @ horizontal)
# set focal point to objects' center
# offset camera position from objects center by dist in opposite of viewing direction
# (actual distance doesn't matter due to parallel projection)
dist = 1
camera_position = position + dist * direction
self.SetViewUp(*viewup)
self.SetPosition(*camera_position)
self.SetFocalPoint(*position)
ps = max(horiz_dist / aspect[0], vert_dist) / 2
self.parallel_scale = ps * (1 + padding)
self._renderer.ResetCameraClippingRange(x0, x1, y0, y1, z0, z1)
if adjust_render_window:
ren_win = self._renderer.GetRenderWindow()
size = list(ren_win.GetSize())
size_ratio = size[0] / size[1]
tight_ratio = horiz_dist / vert_dist
resize_ratio = tight_ratio / size_ratio
if resize_ratio < 1:
size[0] = round(size[0] * resize_ratio)
else:
size[1] = round(size[1] / resize_ratio)
ren_win.SetSize(size)
# simply call tight again to reset the parallel scale due to the
# resized window
self.tight(padding=padding, adjust_render_window=False, view=view, negative=negative)
self.is_set = True

Some files were not shown because too many files have changed in this diff Show More