Skip to content

Commit

Permalink
Add vertices support (#200)
Browse files Browse the repository at this point in the history
* add vertices support

* fix version

* remove numpypin

* add coverage for dataset method

* make testing more robust

* fix errors on older vtk for tests

* Fix check

* make tests better by includign 1 vertex and 1 polyvertex example
  • Loading branch information
MatthewFlamm authored Jul 16, 2024
1 parent 938d7a4 commit 7bfba6e
Show file tree
Hide file tree
Showing 12 changed files with 359 additions and 7 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ PYthon RAndom SAmpling for MEshes
[Documentation](https://matthewflamm.github.io/pyransame/)

Utilities for choosing random samples of points within cells of [PyVista](https://github.com/pyvista/pyvista) meshes.
This package does _not_ choose random points that define the mesh itself, rather random points on the 1D line, 2D surface or
in the 3D volume are sampled.
This package does _not_ choose random points that define the mesh itself, rather random points on 0D vertices, 1D lines, 2D surfaces or
in 3D volumes are sampled.

All linear[^1] cells from [vtk](https://gitlab.kitware.com/vtk/vtk) are supported, except for `vtkConvexPointSet`.

[^1]: Linear here means not inheriting from `vtkNonLinearCell`.

## Random sampling on a 2D surface

Expand Down
2 changes: 2 additions & 0 deletions doc/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ API Documentation
random_volume_dataset
random_line_points
random_line_dataset
random_vertex_points
random_vertex_dataset
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ dynamic = ["version"]
name = "pyransame"
requires-python = ">=3.8"
dependencies = [
"pyvista>=0.42",
"pyvista>=0.44",
"numpy",
]
description = "PYthon RAndom SAmpling for MEshes"
Expand Down
2 changes: 1 addition & 1 deletion requirements-doc.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ jupyter_sphinx==0.5.3
jupyterlab==4.2.3
numpydoc==1.7.0
pydata-sphinx-theme==0.15.4
pyvista==0.43.10
pyvista==0.44.0
sphinx==7.3.7
sphinx-design==0.6.0
sphinx-gallery==0.16.0
Expand Down
1 change: 1 addition & 0 deletions requirements-test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pytest==8.2.2
pytest-cov==5.0.0
hypothesis==6.104.2
mypy==1.10.1
pyvista==0.44.0
1 change: 1 addition & 0 deletions src/pyransame/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@

from .line import random_line_dataset, random_line_points
from .surface import random_surface_dataset, random_surface_points
from .vertex import random_vertex_dataset, random_vertex_points
from .volume import random_volume_dataset, random_volume_points
8 changes: 8 additions & 0 deletions src/pyransame/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,11 @@ def _generate_points_in_polyline(points: np.ndarray, n: int = 1):
def _length_line(points: np.ndarray):
a, b = points
return np.linalg.norm(a - b)


def _generate_points_in_vertex(points: np.ndarray, n: int = 1):
return np.repeat(points, n, 0)


def _generate_points_in_polyvertex(points: np.ndarray, n: int = 1):
return pyransame.rng.choice(points, n)
183 changes: 183 additions & 0 deletions src/pyransame/vertex.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
"""Generating random points from vertices."""

from typing import Optional, Union

import numpy as np
import numpy.typing as npt
import pyvista as pv

import pyransame
import pyransame.util as util


def random_vertex_points(
mesh: pv.DataSet,
n: int = 1,
weights: Optional[Union[str, npt.ArrayLike]] = None,
) -> np.ndarray:
"""
Generate random points from vertices.
.. note::
This function is provided for completeness of API,
but it is likely faster and more flexible to use
a custom method.
Supported cell types:
- Vertex
- PolyVertex
Parameters
----------
mesh : pyvista.DataSet
The mesh for which to generate random points. Must have cells.
n : int, default: 1
Number of random points to generate.
weights : str, or array_like, optional
Weights to use for probability of choosing points inside each cell.
If a ``str`` is supplied, it will use the existing cell data on ``mesh``.
Returns
-------
points : np.ndarray
``(n, 3)`` points that exist inside cells on ``mesh``.
Examples
--------
Create a mesh with 1 vertex cell (1 point) and 1 polyvertex cell (5 points).
>>> import pyransame
>>> import pyvista as pv
>>> p = [
... [0., 0., 0.],
... [1., 0., 0.],
... [1., 1., 0.],
... [1., 2., 0.],
... [1., 3., 0.],
... [1., 4., 0.],
... ]
>>> mesh = pv.PolyData(p, verts=[1, 0, 5, 1, 2, 3, 4, 5])
>>> points = pyransame.random_vertex_points(mesh, n=3)
Now plot result.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, render_points_as_spheres=True, point_size=16.0, color='blue')
>>> _ = pl.add_points(points, render_points_as_spheres=True, point_size=20.0, color='red')
>>> pl.view_xy()
>>> pl.show()
"""
if weights is None:
weights = np.ones(mesh.n_cells)

if isinstance(weights, str):
weights = mesh.cell_data[weights]

weights = np.asanyarray(weights)

if n <= 0:
raise ValueError(f"n must be > 0, got {n}")

n_cells = mesh.n_cells

if "VertexCount" not in mesh.cell_data:
mesh = mesh.compute_cell_sizes(length=False, area=False, volume=False, vertex_count=True) # type: ignore

p = weights * mesh["VertexCount"]

if p.sum() == 0:
raise ValueError("No cells with vertices in DataSet")

p = p / p.sum()

chosen_cells, unique_counts, point_indices = util._random_cells(n_cells, n, p)
points = np.empty((n, 3))
for i, (chosen_cell, count) in enumerate(zip(chosen_cells, unique_counts)):
c = mesh.get_cell(chosen_cell)
if c.type == pv.CellType.VERTEX:
points[point_indices[i] : point_indices[i + 1], :] = (
util._generate_points_in_vertex(c.points, count)
)
elif c.type == pv.CellType.POLY_VERTEX:
points[point_indices[i] : point_indices[i + 1], :] = (
util._generate_points_in_polyvertex(c.points, count)
)
else:
raise NotImplementedError(
f"Random generation for {c.type.name} not yet supported"
)
return points


def random_vertex_dataset(
mesh: pv.DataSet,
n: int = 1,
weights: Optional[Union[str, npt.ArrayLike]] = None,
) -> pv.PolyData:
"""
Generate random points on vertices with sampled data.
.. note::
This function is provided for completeness of API,
but it is likely faster and more flexible to use
a custom method.
Supported cell types:
- Vertex
- PolyVertex
Parameters
----------
mesh : pyvista.DataSet
The mesh for which to generate random points. Must have cells.
n : int, default: 1
Number of random points to generate.
weights : str, or array_like, optional
Weights to use for probability of choosing points inside each cell.
If a ``str`` is supplied, it will use the existing cell data on ``mesh``.
Returns
-------
points : pv.PolyData
``(n, 3)`` points that exist inside cells on ``mesh`` and with
sampled data.
Examples
--------
Create a mesh with 1 vertex cell (1 point) and 1 polyvertex cell (5 points).
Add data for y position.
>>> import pyransame
>>> import pyvista as pv
>>> p = [
... [0., 0., 0.],
... [1., 0., 0.],
... [1., 1., 0.],
... [1., 2., 0.],
... [1., 3., 0.],
... [1., 4., 0.],
... ]
>>> mesh = pv.PolyData(p, verts=[1, 0, 5, 1, 2, 3, 4, 5])
>>> mesh["y"] = mesh.points[:, 1]
>>> dataset = pyransame.random_vertex_dataset(mesh, n=3)
Now plot result.
>>> pl = pv.Plotter()
>>> _ = pl.add_mesh(mesh, render_points_as_spheres=True, point_size=8.0, color='black')
>>> _ = pl.add_points(dataset, render_points_as_spheres=True, point_size=20.0, scalars="y")
>>> pl.view_xy()
>>> pl.show()
"""
points = random_vertex_points(mesh, n, weights)
return pv.PolyData(points).sample(
mesh, locator="static_cell", snap_to_closest_point=True
)
17 changes: 17 additions & 0 deletions tests/test_random_dataset.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Tests for random_*_dataset."""

import numpy as np
import pytest
import pyvista as pv

import pyransame
Expand Down Expand Up @@ -40,3 +41,19 @@ def test_random_line_dataset():
assert np.allclose(sampled.points[:, 0], sampled["x"])
assert np.allclose(sampled.points[:, 1], sampled["y"])
assert np.allclose(sampled.points[:, 2], sampled["z"])


@pytest.mark.skipif(
pv.vtk_version_info < (9, 3), reason="requires vtk ve4sion 9.3 or higher"
)
def test_random_vertex_dataset():
mesh = pv.ImageData(dimensions=(10, 10, 10))
mesh = pv.PolyData(mesh.points) # make all vertex cells
mesh.point_data["x"] = mesh.points[:, 0]
mesh.point_data["y"] = mesh.points[:, 1]
mesh.point_data["z"] = mesh.points[:, 2]

sampled = pyransame.random_vertex_dataset(mesh, 50)
assert np.allclose(sampled.points[:, 0], sampled["x"])
assert np.allclose(sampled.points[:, 1], sampled["y"])
assert np.allclose(sampled.points[:, 2], sampled["z"])
6 changes: 3 additions & 3 deletions tests/test_random_line_points.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_nonuniform_cell_size(nonuniform_line):


def test_nonuniform_cell_size_w_precomputed_areas(nonuniform_line):
mesh = nonuniform_line.compute_cell_sizes(length=False, volume=False)
mesh = nonuniform_line.compute_cell_sizes(length=True, area=False, volume=False)

points = pyransame.random_line_points(mesh, 200000)
assert np.allclose(points.mean(axis=0), (-0.5, 0.0, 0.0), rtol=5e-3, atol=5e-3)
Expand Down Expand Up @@ -117,9 +117,9 @@ def test_wrong_weights(line):
weights = {"not a good entry": "should raise an error"}

with pytest.raises(TypeError):
pyransame.random_surface_points(line, 20, weights=weights)
pyransame.random_line_points(line, 20, weights=weights)


def test_wrong_n(line):
with pytest.raises(ValueError, match="n must be > 0, got -20"):
pyransame.random_surface_points(line, -20)
pyransame.random_line_points(line, -20)
Loading

0 comments on commit 7bfba6e

Please sign in to comment.