Skip to content

test/func: created a test and function for MorphFuncy #186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Apr 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions news/morphfuncy.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* General morph function that applies a user-supplied Python function to the y-coordinates of morph data

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
68 changes: 68 additions & 0 deletions src/diffpy/morph/morphs/morphfuncy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this would be easier to read with judiciously placed blank lines, maybe after each code block, but play around till it looks nice? I am not sure if there is a docstring standard for this, in which case follow it, but if not, then I think a few blank lines could increase readability.

>>> 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.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
74 changes: 74 additions & 0 deletions tests/test_morphfuncy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import numpy as np
import pytest

from diffpy.morph.morphs.morphfuncy import MorphFuncy

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these functions would be better with single blank lines between them.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to delete these but every time I commit the files are modified by the hooks. I think this is the pre-commit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK,, we can let it slide. It is because normally we want the functions well separated, but wehn we are defining a bunch of them for testing we just want them kind of closed up, but it is not a big deal.

Just fix the docstring and I can merge this.


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 = (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason you have the function call inside parens? It should work the same way without these and this is somewhat unconventional and therefore distracting?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is same as comment above, this is modified by the hooks.

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)
Loading