Skip to content
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

metatensor-operations finite difference test for add #479

Merged
merged 2 commits into from
Jul 3, 2024
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
14 changes: 14 additions & 0 deletions python/metatensor-operations/metatensor/operations/_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ def _check_all_np_ndarray(arrays):
)


def sum(array, axis: Optional[int] = None):
"""
Returns the sum of the elements in the array at the axis.

It is equivalent of np.sum(array, axis=axis) and torch.sum(tensor, dim=axis)
"""
if isinstance(array, TorchTensor):
return torch.sum(array, dim=axis)
elif isinstance(array, np.ndarray):
return np.sum(array, axis=axis).astype(array.dtype)
else:
raise TypeError(UNKNOWN_ARRAY_TYPE)


def abs(array):
"""
Returns the absolute value of the elements in the array.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import numpy as np

from . import _dispatch
from ._backend import Labels, TensorBlock, torch_jit_script
from ._backend import Labels, TensorBlock, torch_jit_is_scripting, torch_jit_script


try:
import torch

TorchScriptClass = torch.ScriptClass
except ImportError:

class TorchScriptClass:
pass


@torch_jit_script
Expand Down Expand Up @@ -50,22 +62,37 @@ def block_from_array(array) -> TensorBlock:
must have at least two dimensions. Too few provided: {n_dimensions}"
)

if torch_jit_is_scripting():
# we are using metatensor-torch
labels_array_like = torch.empty(0)
else:
if isinstance(Labels, TorchScriptClass):
# we are using metatensor-torch
labels_array_like = torch.empty(0)
else:
# we are using metatensor-core
labels_array_like = np.empty(0)

samples = Labels(
names=["sample"],
values=_dispatch.int_array_like(list(range(shape[0])), array).reshape(-1, 1),
values=_dispatch.int_array_like(
list(range(shape[0])), labels_array_like
).reshape(-1, 1),
)
components = [
Labels(
names=[f"component_{component_index+1}"],
values=_dispatch.int_array_like(list(range(axis_size)), array).reshape(
-1, 1
),
values=_dispatch.int_array_like(
list(range(axis_size)), labels_array_like
).reshape(-1, 1),
)
for component_index, axis_size in enumerate(shape[1:-1])
]
properties = Labels(
names=["property"],
values=_dispatch.int_array_like(list(range(shape[-1])), array).reshape(-1, 1),
values=_dispatch.int_array_like(
list(range(shape[-1])), labels_array_like
).reshape(-1, 1),
)

device = _dispatch.get_device(array)
Expand Down
152 changes: 152 additions & 0 deletions python/metatensor-operations/tests/_gradcheck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import numpy as np

import metatensor
from metatensor import Labels, TensorBlock, TensorMap
from metatensor.operations import _dispatch


def check_finite_differences(
function,
array,
*,
parameter: str,
displacement: float = 1e-6,
rtol: float = 1e-5,
atol: float = 1e-15,
) -> None:
"""
Check that analytical gradients with respect to ``parameter`` in the
:py:class:`TensorMap` returned by ``function`` agree with finite differences.

The ``function`` must take an array (either torch or numpy) and return a
:py:class:`TensorMap`. All the blocks in the returned TensorMap should have one
sample per row of the ``array``, and the gradient-specific components must match the
other dimensions of the ``array``.
"""
n_samples = array.shape[0]
n_grad_components = array.shape[1:]

reference = function(array)

values_components = reference.block(0).components
grad_components = reference.block(0).gradient(parameter).components

assert len(grad_components) == len(values_components) + len(n_grad_components)

for sample_i in range(n_samples):
for grad_components_i in np.ndindex(n_grad_components):
array_pos = _dispatch.copy(array)
index = (sample_i,) + grad_components_i
array_pos[index] += displacement / 2
updated_pos = function(array_pos)

array_neg = _dispatch.copy(array)
array_neg[index] -= displacement / 2
updated_neg = function(array_neg)

assert updated_pos.keys == reference.keys
assert updated_neg.keys == reference.keys

for key, block in reference.items():
gradients = block.gradient(parameter)

block_pos = updated_pos.block(key)
block_neg = updated_neg.block(key)

for gradient_i, gradient_sample in enumerate(gradients.samples):
current_sample_i = gradient_sample[0]
if current_sample_i != sample_i:
continue

assert block_pos.samples[sample_i] == block.samples[sample_i]
assert block_neg.samples[sample_i] == block.samples[sample_i]

value_pos = block_pos.values[sample_i]
value_neg = block_neg.values[sample_i]

grad_index = (gradient_i,) + grad_components_i
gradient = gradients.values[grad_index]

assert value_pos.shape == gradient.shape
assert value_neg.shape == gradient.shape

finite_difference = (value_pos - value_neg) / displacement

np.testing.assert_allclose(
finite_difference,
gradient,
rtol=rtol,
atol=atol,
)


def cartesian_cubic(array) -> TensorMap:
"""
Creates a TensorMap from a set of cartesian vectors according to the function:

.. math::

f(x, y, z) = x^3 + y^3 + z^3

\\nabla f = (3x^2, 3y^2, 3z^2)

"""
n_samples = array.shape[0]
assert array.shape == (n_samples, 3)

values = _dispatch.sum(array**3, axis=1)
values_grad = 3 * array**2

block = metatensor.block_from_array(values.reshape(n_samples, 1))
block.add_gradient(
parameter="g",
gradient=TensorBlock(
values=values_grad.reshape(n_samples, 3, 1),
samples=Labels.range("sample", len(values)),
components=[Labels.range("xyz", 3)],
properties=block.properties,
),
)

return TensorMap(Labels.range("_", 1), [block])


def cartesian_linear(array) -> TensorMap:
"""
Creates a TensorMap from a set of cartesian vectors according to the function:

.. math::

f(x, y, z) = 3x + 2y + 8*z + 4

\\nabla f = (3, 2, 8)

"""
n_samples = array.shape[0]
assert array.shape == (n_samples, 3)

values = 3 * array[:, 0] + 2 * array[:, 1] + 8 * array[:, 2] + 4

values_grad = _dispatch.zeros_like(array, (n_samples, 3, 1))
values_grad[:, 0] = 3 * _dispatch.ones_like(array, (n_samples, 1))
values_grad[:, 1] = 2 * _dispatch.ones_like(array, (n_samples, 1))
values_grad[:, 2] = 8 * _dispatch.ones_like(array, (n_samples, 1))

block = metatensor.block_from_array(values.reshape(-1, 1))
block.add_gradient(
parameter="g",
gradient=TensorBlock(
values=values_grad,
samples=Labels.range("sample", len(values)),
components=[Labels.range("xyz", 3)],
properties=block.properties,
),
)

return TensorMap(Labels.range("_", 1), [block])


def test_basic_functions():
array = np.random.rand(42, 3)
check_finite_differences(cartesian_cubic, array, parameter="g")
check_finite_differences(cartesian_linear, array, parameter="g")
31 changes: 31 additions & 0 deletions python/metatensor-operations/tests/add.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@
import metatensor
from metatensor import Labels, TensorBlock, TensorMap

from . import _gradcheck


try:
import torch

HAS_TORCH = True
except ImportError:
HAS_TORCH = False


@pytest.fixture
def keys():
Expand Down Expand Up @@ -258,3 +268,24 @@ def test_self_add_error():
)
with pytest.raises(TypeError, match=message):
metatensor.add(tensor, np.ones((3, 4)))


def test_add_finite_difference():
def function(array):
tensor_1 = _gradcheck.cartesian_linear(array)
tensor_2 = _gradcheck.cartesian_cubic(array)
return metatensor.add(tensor_1, tensor_2)

array = np.random.rand(5, 3)
_gradcheck.check_finite_differences(function, array, parameter="g")


@pytest.mark.skipif(not HAS_TORCH, reason="requires torch")
def test_torch_add_finite_difference():
def function(array):
tensor_1 = _gradcheck.cartesian_linear(array)
tensor_2 = _gradcheck.cartesian_cubic(array)
return metatensor.add(tensor_1, tensor_2)

array = torch.rand(5, 3, dtype=torch.float64)
_gradcheck.check_finite_differences(function, array, parameter="g")
33 changes: 33 additions & 0 deletions python/metatensor-operations/tests/block_from_array.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@
import metatensor


try:
import torch

HAS_TORCH = True
except ImportError:
HAS_TORCH = False


@pytest.mark.parametrize("n_axes", [0, 1])
def test_too_few_axes(n_axes):
"""Test block_from_array when too few axes are provided."""
Expand Down Expand Up @@ -50,3 +58,28 @@ def test_with_components():
np.testing.assert_equal(
block.properties.values, np.arange(array.shape[2]).reshape((-1, 1))
)


@pytest.mark.skipif(not HAS_TORCH, reason="requires torch")
def test_torch_with_components():
Luthaf marked this conversation as resolved.
Show resolved Hide resolved
"""Test block_from_array with components and torch arrays"""
array = array = torch.zeros((6, 5, 7))
block = metatensor.block_from_array(array)
assert block.values is array

assert block.samples.names == ["sample"]
np.testing.assert_equal(
block.samples.values, np.arange(array.shape[0]).reshape((-1, 1))
)

assert len(block.components) == 1
component = block.components[0]
assert component.names == ["component_1"]
np.testing.assert_equal(
component.values, np.arange(array.shape[1]).reshape((-1, 1))
)

assert block.properties.names == ["property"]
np.testing.assert_equal(
block.properties.values, np.arange(array.shape[2]).reshape((-1, 1))
)
Loading