Построение параметрических объектов в виде сетки в PyVista
Я застрял, вероятно, с простой проблемой, но после прочтения документации pyvista я все еще ищу ответ. Я пытаюсь построить сетку, в которой каждая ячейка будет сеткой, определенной как параметрическая форма, то есть супертор. В ранней версии pyvista я определил "свой собственный" суперторус следующим образом:
def supertorus(yScale, xScale, Height, InternalRadius, Vertical, Horizontal,
deltaX=0, deltaY=0, deltaZ=0):
# initial range for values used in parametric equation
n = 100
u = np.linspace(-np.pi, np.pi, n)
t = np.linspace(-np.pi, np.pi, n)
u, t = np.meshgrid(u, t)
# a1: Y Scale <0, 2>
a1 = yScale
# a2: X Scale <0, 2>
a2 = xScale
# a3: Height <0, 5>
a3 = Height
# a4: Internal radius <0, 5>
a4 = InternalRadius
# e1: Vertical squareness <0.25, 1>
e1 = Vertical
# e2: Horizontal squareness <0.25, 1>
e2 = Horizontal
# Definition of parametric equation for supertorus
x = a1 * (a4 + np.sign(np.cos(u)) * np.abs(np.cos(u)) ** e1) *\
np.sign(np.cos(t)) * np.abs(np.cos(t)) ** e2
y = a2 * (a4 + np.sign(np.cos(u)) * np.abs(np.cos(u)) ** e1) *\
np.sign(np.sin(t)) * np.abs(np.sin(t)) ** e2
z = a3 * np.sign(np.sin(u)) * np.abs(np.sin(u)) ** e1
grid = pyvista.StructuredGrid(x + deltaX + 5, y + deltaY + 5, z + deltaZ)
return grid
Я мог манипулировать deltaX
, deltaY
а также deltaZ
разместить supertori в выбранном мной месте. К сожалению, этот подход оказался неэффективным, и я планирую использовать предоставленные PyVista супертороидальные сетки ( https://docs.pyvista.org/examples/00-load/create-parametric-geometric-objects.html?highlight=supertoroid). Мой вопрос: как я могу разместить несколько сеток (например, supertori) в месте, определенном координатамиx
, y
, z
?
1 ответ
Я считаю, что вы ищете глифы. Вы можете передать свой собственный набор данных в виде геометрии глифа, который, в свою очередь, будет отображать набор данных в каждой точке суперсетки. Не вдаваясь в детали ориентации ваших глифов, их раскраски в соответствии со скалярами и прочим, вот простой сценарий "вторжения пришельцев" в качестве примера:
import numpy as np
import pyvista as pv
# get dataset for the glyphs: supertoroid in xy plane
saucer = pv.ParametricSuperToroid(ringradius=0.5, n2=1.5, zradius=0.5)
saucer.rotate_y(90)
# saucer.plot() # <-- check how a single saucer looks like
# get dataset where to put glyphs
x,y,z = np.mgrid[-1:2, -1:2, :2]
mesh = pv.StructuredGrid(x, y, z)
# construct the glyphs on top of the mesh
glyphs = mesh.glyph(geom=saucer, factor=0.3)
# glyphs.plot() # <-- simplest way to plot it
# create Plotter and add our glyphs with some nontrivial lighting
plotter = pv.Plotter(window_size=(1000, 800))
plotter.add_mesh(glyphs, color=[0.2, 0.2, 0.2], specular=1, specular_power=15)
plotter.show()
Я добавил сильное зеркальное освещение, чтобы блюдца выглядели более угрожающе:
Но ключевой момент для вашей проблемы было создание глифов из вашего supermesh, передав его в geom
ключевое слово mesh.glyph
. Другие ключевые слова, такие какorient
а также scale
полезны для глифов в виде стрелок, где вы можете использовать глиф для обозначения векторной информации вашего набора данных.
В комментариях вы спрашивали, можно ли изменять глифы в наборе данных. Я был уверен, что это невозможно, однако в документации VTK четко упоминается возможность определения набора используемых глифов:
Можно использовать более одного глифа, создав таблицу исходных объектов, каждый из которых определяет свой глиф. Если таблица глифов определена, то таблица может быть проиндексирована с использованием либо скалярного значения, либо величины вектора.
Оказывается, что PyVista
не раскрывает эту функциональность (пока), но базовая vtk
пакет позволяет нам испачкать руки. Вот доказательство концепции, основанной наDataSetFilters.glyph
, который я использую разработчиками PyVista, чтобы узнать, есть ли интерес в раскрытии этой функции.
import numpy as np
import pyvista as pv
from pyvista.core.filters import _get_output # just for this standalone example
import vtk
pyvista = pv # just for this standalone example
# below: adapted from core/filters.py
def multiglyph(dataset, orient=True, scale=True, factor=1.0,
tolerance=0.0, absolute=False, clamping=False, rng=None,
geom_datasets=None, geom_values=None):
"""Copy a geometric representation (called a glyph) to every point in the input dataset.
The glyphs may be oriented along the input vectors, and they may be scaled according to scalar
data or vector magnitude.
Parameters
----------
orient : bool
Use the active vectors array to orient the glyphs
scale : bool
Use the active scalars to scale the glyphs
factor : float
Scale factor applied to sclaing array
tolerance : float, optional
Specify tolerance in terms of fraction of bounding box length.
Float value is between 0 and 1. Default is 0.0. If ``absolute``
is ``True`` then the tolerance can be an absolute distance.
absolute : bool, optional
Control if ``tolerance`` is an absolute distance or a fraction.
clamping: bool
Turn on/off clamping of "scalar" values to range.
rng: tuple(float), optional
Set the range of values to be considered by the filter when scalars
values are provided.
geom_datasets : tuple(vtk.vtkDataSet), optional
The geometries to use for the glyphs in table mode
geom_values : tuple(float), optional
The value to assign to each geometry dataset, optional
"""
# Clean the points before glyphing
small = pyvista.PolyData(dataset.points)
small.point_arrays.update(dataset.point_arrays)
dataset = small.clean(point_merging=True, merge_tol=tolerance,
lines_to_points=False, polys_to_lines=False,
strips_to_polys=False, inplace=False,
absolute=absolute)
# Make glyphing geometry
if not geom_datasets:
arrow = vtk.vtkArrowSource()
arrow.Update()
geom_datasets = arrow.GetOutput(),
geom_values = 0,
# check if the geometry datasets are consistent
if not len(geom_datasets) == len(geom_values):
raise ValueError('geom_datasets and geom_values must have the same length!')
# TODO: other kinds of sanitization, e.g. check for sequences etc.
# Run the algorithm
alg = vtk.vtkGlyph3D()
if len(geom_values) == 1:
# use a single glyph
alg.SetSourceData(geom_datasets[0])
else:
alg.SetIndexModeToScalar()
# TODO: index by vectors?
# TODO: SetInputArrayToProcess for arbitrary arrays, maybe?
alg.SetRange(min(geom_values), max(geom_values))
# TODO: different Range?
for val, geom in zip(geom_values, geom_datasets):
alg.SetSourceData(val, geom)
if isinstance(scale, str):
dataset.active_scalars_name = scale
scale = True
if scale:
if dataset.active_scalars is not None:
if dataset.active_scalars.ndim > 1:
alg.SetScaleModeToScaleByVector()
else:
alg.SetScaleModeToScaleByScalar()
else:
alg.SetScaleModeToDataScalingOff()
if isinstance(orient, str):
dataset.active_vectors_name = orient
orient = True
if rng is not None:
alg.SetRange(rng)
alg.SetOrient(orient)
alg.SetInputData(dataset)
alg.SetVectorModeToUseVector()
alg.SetScaleFactor(factor)
alg.SetClamping(clamping)
alg.Update()
return _get_output(alg)
def example():
"""Small glyph example"""
rng = np.random.default_rng()
# get dataset for the glyphs: supertoroid in xy plane
# use N random kinds of toroids over a mesh with 27 points
N = 5
values = np.arange(N) # values for scalars to look up glyphs by
geoms = [pv.ParametricSuperToroid(n1=n1, n2=n2) for n1,n2 in rng.uniform(0.5, 2, size=(N, 2))]
for geom in geoms:
# make the disks horizontal for aesthetics
geom.rotate_y(90)
# get dataset where to put glyphs
x,y,z = np.mgrid[-1:2, -1:2, -1:2]
mesh = pv.StructuredGrid(x, y, z)
# add random scalars
mesh.point_arrays['scalars'] = rng.integers(0, N, size=x.size)
# construct the glyphs on top of the mesh; don't scale by scalars now
glyphs = multiglyph(mesh, geom_datasets=geoms, geom_values=values, scale=False, factor=0.3)
# create Plotter and add our glyphs with some nontrivial lighting
plotter = pv.Plotter(window_size=(1000, 800))
plotter.add_mesh(glyphs, specular=1, specular_power=15)
plotter.show()
if __name__ == "__main__":
example()
В multiglyph
функция в приведенном выше в основном такая же, как mesh.glyph
, но я заменил geom
ключевое слово с двумя ключевыми словами, geom_datasets
а также geom_values
. Они определяют отображение индекса -> геометрии, которое затем используется для поиска каждого глифа на основе скаляров массива.
Вы спросили, можете ли вы раскрасить глифы независимо: вы можете. В приведенном выше доказательстве концепции выбор глифа привязан к скалярам (выбор векторов будет столь же легким; я не уверен в произвольных массивах). Однако вы можете легко выбрать, какие массивы окрашивать, когда вы вызываетеpv.Plotter.add_mesh
, поэтому я предлагаю использовать что-то другое, кроме правильных скаляров, для раскрашивания ваших глифов.
Я сохранил скаляры для раскраски, чтобы было легче увидеть различия между глифами. Вы можете видеть, что существует пять различных типов глифов, выбираемых случайным образом на основе случайных скаляров. Если вы установите нецелочисленные скаляры, он все равно будет работать; Я подозреваюvtk
выбирает ближайший скаляр или что-то подобное для поиска.