From e3639a3e549ae3cdcd5ffb7f40e4d5b65b6ffedf Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Wed, 23 Apr 2025 13:51:53 -0600 Subject: [PATCH 1/2] test/func: created a test and function for MorphFuncy --- news/morphfuncy.rst | 23 +++++++++ src/diffpy/morph/morphs/morphfuncy.py | 65 ++++++++++++++++++++++++ tests/test_morphfuncy.py | 73 +++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 news/morphfuncy.rst create mode 100644 src/diffpy/morph/morphs/morphfuncy.py create mode 100644 tests/test_morphfuncy.py diff --git a/news/morphfuncy.rst b/news/morphfuncy.rst new file mode 100644 index 00000000..c6410817 --- /dev/null +++ b/news/morphfuncy.rst @@ -0,0 +1,23 @@ +**Added:** + +* General morph function that applies a user-supplied Python function to the y-coordinates of morph data + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py new file mode 100644 index 00000000..5bcaefe4 --- /dev/null +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -0,0 +1,65 @@ +from diffpy.morph.morphs.morph import LABEL_GR, LABEL_RA, Morph + + +class MorphFuncy(Morph): + """Apply the user-supplied Python function to the y-coordinates of the + morph data""" + + # Define input output types + summary = "Apply a Python function to the y-axis data" + xinlabel = LABEL_RA + yinlabel = LABEL_GR + xoutlabel = LABEL_RA + youtlabel = LABEL_GR + + def morph(self, x_morph, y_morph, x_target, y_target): + """General morph function that applies a user-supplied function to the + y-coordinates of morph data to make it align with a target. + + Configuration Variables + ----------------------- + function: callable + The user-supplied function that applies a transformation to the + y-coordinates of the data. + + parameters: dict + A dictionary of parameters to pass to the function. + These parameters are unpacked using **kwargs. + + Returns + ------- + A tuple (x_morph_out, y_morph_out, x_target_out, y_target_out) + where the target values remain the same and the morph data is + transformed according to the user-specified function and parameters + The morphed data is returned on the same grid as the unmorphed data + + Example + ------- + Import the funcy morph function: + >>> from diffpy.morph.morphs.morphfuncy import MorphFuncy + Define or import the user-supplied transformation function: + >>> def sine_function(x, y, amplitude, frequency): + >>> return amplitude * np.sin(frequency * x) * y + Provide initial guess for parameters: + >>> parameters = {'amplitude': 2, 'frequency': 2} + Run the funcy morph given input morph array (x_morph, y_morph) + and target array (x_target, y_target): + >>> morph = MorphFuncy() + >>> morph.function = sine_function + >>> morph.parameters = parameters + >>> x_morph_out, y_morph_out, x_target_out, y_target_out = morph( + ... x_morph, y_morph, x_target, y_target) + To access parameters from the morph instance: + >>> x_morph_in = morph.x_morph_in + >>> y_morph_in = morph.y_morph_in + >>> x_target_in = morph.x_target_in + >>> y_target_in = morph.y_target_in + >>> parameters_out = morph.parameters + """ + Morph.morph(self, x_morph, y_morph, x_target, y_target) + + self.y_morph_out = self.function( + self.x_morph_in, self.y_morph_in, **self.parameters + ) + + return self.xyallout diff --git a/tests/test_morphfuncy.py b/tests/test_morphfuncy.py new file mode 100644 index 00000000..a84816ce --- /dev/null +++ b/tests/test_morphfuncy.py @@ -0,0 +1,73 @@ +import numpy as np +import pytest + +from diffpy.morph.morphs.morphfuncy import MorphFuncy + + +def sine_function(x, y, amplitude, frequency): + return amplitude * np.sin(frequency * x) * y + + +def exponential_decay_function(x, y, amplitude, decay_rate): + return amplitude * np.exp(-decay_rate * x) * y + + +def gaussian_function(x, y, amplitude, mean, sigma): + return amplitude * np.exp(-((x - mean) ** 2) / (2 * sigma**2)) * y + + +def polynomial_function(x, y, a, b, c): + return (a * x**2 + b * x + c) * y + + +def logarithmic_function(x, y, scale): + return scale * np.log(1 + x) * y + + +@pytest.mark.parametrize( + "function, parameters, expected_function", + [ + ( + sine_function, + {"amplitude": 2, "frequency": 5}, + lambda x, y: 2 * np.sin(5 * x) * y, + ), + ( + exponential_decay_function, + {"amplitude": 5, "decay_rate": 0.1}, + lambda x, y: 5 * np.exp(-0.1 * x) * y, + ), + ( + gaussian_function, + {"amplitude": 1, "mean": 5, "sigma": 1}, + lambda x, y: np.exp(-((x - 5) ** 2) / (2 * 1**2)) * y, + ), + ( + polynomial_function, + {"a": 1, "b": 2, "c": 0}, + lambda x, y: (x**2 + 2 * x) * y, + ), + ( + logarithmic_function, + {"scale": 0.5}, + lambda x, y: 0.5 * np.log(1 + x) * y, + ), + ], +) +def test_funcy(function, parameters, expected_function): + x_morph = np.linspace(0, 10, 101) + y_morph = np.sin(x_morph) + x_target = x_morph.copy() + y_target = y_morph.copy() + x_morph_expected = x_morph + y_morph_expected = expected_function(x_morph, y_morph) + morph = MorphFuncy() + morph.function = function + morph.parameters = parameters + x_morph_actual, y_morph_actual, x_target_actual, y_target_actual = ( + morph.morph(x_morph, y_morph, x_target, y_target) + ) + assert np.allclose(y_morph_actual, y_morph_expected) + assert np.allclose(x_morph_actual, x_morph_expected) + assert np.allclose(x_target_actual, x_target) + assert np.allclose(y_target_actual, y_target) From b2c46cbd90f6438061e1b49d6310ba4fa8c99054 Mon Sep 17 00:00:00 2001 From: Luis Kitsu Iglesias Date: Thu, 24 Apr 2025 09:51:06 -0600 Subject: [PATCH 2/2] test/func: improving readability --- src/diffpy/morph/morphs/morphfuncy.py | 7 +++++-- tests/test_morphfuncy.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/diffpy/morph/morphs/morphfuncy.py b/src/diffpy/morph/morphs/morphfuncy.py index 5bcaefe4..b9d65f2f 100644 --- a/src/diffpy/morph/morphs/morphfuncy.py +++ b/src/diffpy/morph/morphs/morphfuncy.py @@ -37,18 +37,22 @@ def morph(self, x_morph, y_morph, x_target, y_target): ------- Import the funcy morph function: >>> from diffpy.morph.morphs.morphfuncy import MorphFuncy + Define or import the user-supplied transformation function: >>> def sine_function(x, y, amplitude, frequency): >>> return amplitude * np.sin(frequency * x) * y + Provide initial guess for parameters: >>> parameters = {'amplitude': 2, 'frequency': 2} + Run the funcy morph given input morph array (x_morph, y_morph) and target array (x_target, y_target): >>> morph = MorphFuncy() >>> morph.function = sine_function >>> morph.parameters = parameters - >>> x_morph_out, y_morph_out, x_target_out, y_target_out = morph( + >>> x_morph_out, y_morph_out, x_target_out, y_target_out = morph.morph( ... x_morph, y_morph, x_target, y_target) + To access parameters from the morph instance: >>> x_morph_in = morph.x_morph_in >>> y_morph_in = morph.y_morph_in @@ -61,5 +65,4 @@ def morph(self, x_morph, y_morph, x_target, y_target): self.y_morph_out = self.function( self.x_morph_in, self.y_morph_in, **self.parameters ) - return self.xyallout diff --git a/tests/test_morphfuncy.py b/tests/test_morphfuncy.py index a84816ce..a73a8096 100644 --- a/tests/test_morphfuncy.py +++ b/tests/test_morphfuncy.py @@ -67,6 +67,7 @@ def test_funcy(function, parameters, expected_function): x_morph_actual, y_morph_actual, x_target_actual, y_target_actual = ( morph.morph(x_morph, y_morph, x_target, y_target) ) + assert np.allclose(y_morph_actual, y_morph_expected) assert np.allclose(x_morph_actual, x_morph_expected) assert np.allclose(x_target_actual, x_target)