Skip to content

Commit b0adc1f

Browse files
authored
Add method to export ellipsoids as PyVista objects (#614)
Add a `to_pyvista` method to all ellipsoid classes to export them to PyVista objects that can be either plotted by it, or exported as VTK files. Add tests for the new method, running it on all ellipsoid types.
1 parent db072a1 commit b0adc1f

File tree

2 files changed

+87
-0
lines changed

2 files changed

+87
-0
lines changed

harmonica/_forward/ellipsoids/ellipsoids.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
from ..utils import get_rotation_matrix
1818

19+
try:
20+
import pyvista
21+
except ImportError:
22+
pyvista = None
23+
1924

2025
def create_ellipsoid(
2126
a: float,
@@ -258,6 +263,37 @@ def _check_positive_semiaxis(self, value: float, semiaxis: str):
258263
msg = f"Invalid value of '{semiaxis}' equal to '{value}'. It must be positive."
259264
raise ValueError(msg)
260265

266+
def to_pyvista(self, **kwargs):
267+
"""
268+
Export ellipsoid to a :class:`pyvista.PolyData` object.
269+
270+
.. important::
271+
272+
The :mod:`pyvista` optional dependency must be installed to use this method.
273+
274+
Parameters
275+
----------
276+
kwargs : dict
277+
Keyword arguments passed to :func:`pyvista.ParametricEllipsoid`.
278+
279+
Returns
280+
-------
281+
ellipsoid : pyvista.PolyData
282+
A PyVista's parametric ellipsoid.
283+
"""
284+
if pyvista is None:
285+
msg = (
286+
"Missing optional dependency 'pyvista' required for "
287+
"exporting ellipsoids to PyVista."
288+
)
289+
raise ImportError(msg)
290+
ellipsoid = pyvista.ParametricEllipsoid(
291+
xradius=self.a, yradius=self.b, zradius=self.c, **kwargs
292+
)
293+
ellipsoid.rotate(rotation=self.rotation_matrix, inplace=True)
294+
ellipsoid.translate(self.center, inplace=True)
295+
return ellipsoid
296+
261297

262298
class TriaxialEllipsoid(BaseEllipsoid):
263299
"""

harmonica/tests/ellipsoids/test_ellipsoids.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import itertools
1212
import re
13+
from unittest.mock import patch
1314

1415
import numpy as np
1516
import pytest
@@ -22,6 +23,11 @@
2223
create_ellipsoid,
2324
)
2425

26+
try:
27+
import pyvista
28+
except ImportError:
29+
pyvista = None
30+
2531

2632
class TestProlateEllipsoid:
2733
"""Test the ProlateEllipsoid class."""
@@ -544,3 +550,48 @@ def test_physical_properties(self, a, b, c, physical_property):
544550
center = (1.0, 2.0, 3.0)
545551
ellipsoid = create_ellipsoid(a, b, c, center=center, **kwargs)
546552
np.testing.assert_allclose(getattr(ellipsoid, physical_property), value)
553+
554+
555+
@pytest.mark.skipif(pyvista is None, reason="requires pyvista")
556+
class TestToPyvista:
557+
"""Test exporting ellipsoids to PyVista objects."""
558+
559+
@pytest.fixture(params=["oblate", "prolate", "triaxial", "sphere"])
560+
def ellipsoid(self, request):
561+
a, b, c = 3.0, 2.0, 1.0
562+
yaw, pitch, roll = 73.0, 14.0, -35.0
563+
center = (43.0, -72.0, 105)
564+
match request.param:
565+
case "oblate":
566+
ellipsoid = OblateEllipsoid(b, a, yaw, pitch, center=center)
567+
case "prolate":
568+
ellipsoid = ProlateEllipsoid(a, b, yaw, pitch, center=center)
569+
case "triaxial":
570+
ellipsoid = TriaxialEllipsoid(a, b, c, yaw, pitch, roll, center=center)
571+
case "sphere":
572+
ellipsoid = Sphere(a, center=center)
573+
case _:
574+
raise ValueError()
575+
return ellipsoid
576+
577+
@patch("harmonica._forward.ellipsoids.ellipsoids.pyvista", None)
578+
def test_pyvista_missing_error(self, ellipsoid):
579+
"""
580+
Check if error is raised when pyvista is not installed.
581+
"""
582+
with pytest.raises(ImportError):
583+
ellipsoid.to_pyvista()
584+
585+
def test_pyvista_object(self, ellipsoid):
586+
"""
587+
Check if method works as expected.
588+
"""
589+
ellipsoid_pv = ellipsoid.to_pyvista()
590+
assert isinstance(ellipsoid_pv, pyvista.PolyData)
591+
# rtol needed since the parametric ellipsoid is not the exact surface.
592+
np.testing.assert_allclose(ellipsoid_pv.center, ellipsoid.center, rtol=1e-4)
593+
np.testing.assert_allclose(
594+
ellipsoid_pv.volume,
595+
4 / 3 * np.pi * ellipsoid.a * ellipsoid.b * ellipsoid.c,
596+
rtol=1e-3,
597+
)

0 commit comments

Comments
 (0)