diff --git a/ezyrb/approximation/__init__.py b/ezyrb/approximation/__init__.py index 69f06a84..11ea1d88 100644 --- a/ezyrb/approximation/__init__.py +++ b/ezyrb/approximation/__init__.py @@ -9,6 +9,7 @@ "KNeighborsRegressor", "RadiusNeighborsRegressor", "SklearnApproximation", + "CloughTocher", ] from .approximation import Approximation @@ -19,3 +20,4 @@ from .kneighbors_regressor import KNeighborsRegressor from .radius_neighbors_regressor import RadiusNeighborsRegressor from .sklearn_approximation import SklearnApproximation +from .clough_tocher import CloughTocher diff --git a/ezyrb/approximation/clough_tocher.py b/ezyrb/approximation/clough_tocher.py new file mode 100644 index 00000000..dee1af19 --- /dev/null +++ b/ezyrb/approximation/clough_tocher.py @@ -0,0 +1,52 @@ +"""Wrapper for Clough-Tocher 2D Interpolator.""" + +import logging +import numpy as np +from scipy.interpolate import CloughTocher2DInterpolator as CT + +from .approximation import Approximation + +logger = logging.getLogger(__name__) + + +class CloughTocher(Approximation): + r""" + :math:`C^1` smooth, piecewise cubic interpolator for 2D multivariate approximation. + + Note: This interpolator only supports 2-dimensional parameter spaces + (i.e., mapping :math:`\mathbb{R}^2 \to \mathbb{R}^m`). + + :param kwargs: arguments passed to the internal instance of + scipy.interpolate.CloughTocher2DInterpolator. + """ + + def __init__(self, **kwargs): + logger.debug("Initializing CloughTocher with kwargs: %s", kwargs) + super().__init__() + self.kwargs = kwargs + self.interpolator = None + + def fit(self, points, values): + """ + Construct the interpolator given `points` and `values`. + """ + as_np_array = np.array(points) + + # Mathematical constraint: CT only works in R^2 + if as_np_array.ndim != 2 or as_np_array.shape[1] != 2: + logger.error( + "CloughTocher requested for data with shape %s", + as_np_array.shape, + ) + raise ValueError( + "CloughTocher interpolator only supports exactly 2D parameter spaces." + ) + + self.interpolator = CT(as_np_array, values, **self.kwargs) + logger.info("CloughTocher fitted successfully") + + def predict(self, new_point): + """ + Evaluate interpolator at given `new_points`. + """ + return self.interpolator(new_point).squeeze() diff --git a/tests/test_clough_tocher.py b/tests/test_clough_tocher.py new file mode 100644 index 00000000..9c54975d --- /dev/null +++ b/tests/test_clough_tocher.py @@ -0,0 +1,41 @@ +import numpy as np +import pytest + +from ezyrb import CloughTocher + +np.random.seed(17) + +def get_xy(): + npts = 20 + dinput = 2 + + inp = np.random.uniform(-1, 1, size=(npts, dinput)) + out = np.array([ + np.sin(inp[:, 0]) + np.sin(inp[:, 1]**2), + np.cos(inp[:, 0]) + np.cos(inp[:, 1]**2) + ]).T + + return inp, out + +class TestCloughTocher: + def test_constructor_empty(self): + model = CloughTocher() + + def test_fit(self): + x, y = get_xy() + approx = CloughTocher() + approx.fit(x, y) + + def test_predict_01(self): + x, y = get_xy() + approx = CloughTocher() + approx.fit(x, y) + test_y = approx.predict(x) + np.testing.assert_array_almost_equal(y, test_y, decimal=6) + + def test_wrong_dimensions(self): + x = np.random.uniform(-1, 1, size=(10, 3)) + y = np.random.uniform(-1, 1, size=(10, 2)) + approx = CloughTocher() + with pytest.raises(ValueError): + approx.fit(x, y) diff --git a/tests/test_parallel/test_clough_tocher.py b/tests/test_parallel/test_clough_tocher.py new file mode 100644 index 00000000..80886873 --- /dev/null +++ b/tests/test_parallel/test_clough_tocher.py @@ -0,0 +1,7 @@ +import pytest + +import ezyrb +from ezyrb.parallel import ReducedOrderModel as ParallelROM +ezyrb.ReducedOrderModel = ParallelROM + +from tests.test_clough_tocher import *