Skip to content

Commit 1647475

Browse files
committed
Merge branch 'main' into jwa7-patch-1
2 parents 7a4c85c + 2c1c4d2 commit 1647475

File tree

8 files changed

+430
-40
lines changed

8 files changed

+430
-40
lines changed

featomic/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ a changelog](https://keepachangelog.com/en/1.1.0/) format. This project follows
1717
### Removed
1818
-->
1919

20+
### Fixed
21+
- Fixed `featomic.clebsch_gordan.cartesian_to_spherical` transformation for tensors of rank greater than 2 (#371)
22+
- Fixed the calculation of Clebsch-Gordan coefficients, used all across the `featomic.clebsch_gordan` module (#371)
23+
2024
## [Version 0.6.0](https://github.com/metatensor/featomic/releases/tag/featomic-v0.6.0) - 2024-12-20
2125

2226
### Added

python/featomic/featomic/clebsch_gordan/_cartesian_spherical.py

+4
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,10 @@ def cartesian_to_spherical(
245245
# [..., 3, 5, ...] => [..., 3, ...] (o3_lambda=1, o3_sigma=+1)
246246
# => [..., 5, ...] (o3_lambda=2, o3_sigma=-1)
247247
# => [..., 7, ...] (o3_lambda=3, o3_sigma=+1)
248+
249+
# Use the rolled block values as the starting point for higher-rank tensors
250+
tensor = TensorMap(tensor.keys, new_blocks)
251+
248252
if cg_coefficients is None:
249253
if torch_jit_is_scripting():
250254
raise ValueError(

python/featomic/featomic/clebsch_gordan/_coefficients.py

+71-34
Original file line numberDiff line numberDiff line change
@@ -197,23 +197,53 @@ def _build_dense_cg_coeff_dict(
197197
device=device,
198198
dtype=complex_like.dtype,
199199
)
200-
201-
real_cg = (r2c[l1] @ complex_cg.reshape(2 * l1 + 1, -1)).reshape(
202-
complex_cg.shape
203-
)
204-
205-
real_cg = real_cg.swapaxes(0, 1)
206-
real_cg = (r2c[l2] @ real_cg.reshape(2 * l2 + 1, -1)).reshape(
207-
real_cg.shape
208-
)
209-
real_cg = real_cg.swapaxes(0, 1)
210-
211-
real_cg = real_cg @ c2r[o3_lambda]
200+
# We want to create a CG coefficient for a real representation, using
201+
# the expression:
202+
#
203+
# C_{l1m1l2m2}^{lm}
204+
# = \sum_{m'm1'm2'} ...
205+
# ... U_{mm'}^{l} C_{l1m1'm2m2'}^{lm'} ...
206+
# ... U^{\dagger}_{m1'm1}^{l1} U^{\dagger}_{m2'm2}^{l2}
207+
#
208+
# where:
209+
# U is the "c2r" transformation matrix below
210+
# U^{\dagger} is the "r2c" transformation matrix below
211+
#
212+
# 0) We have a CG coefficient of shape:
213+
# complex_cg.shape = (2*l1+1, 2*l2+1, 2*L+1)
214+
# and transormation matrices of shape:
215+
# c2r[L].shape = (2*L+1, 2*L+1)
216+
# r2c[L].shape = (2*L+1, 2*L+1)
217+
#
218+
# 1) take the matrix product:
219+
# \sum_{m1'} C_{l1m1'm2m2'}^{lm'} U^{\dagger}_{m1'm1}^{l1}
220+
# this requires some permuting of axes of the objects involved to
221+
# ensure proper matching for matmul. i.e. permute axes of the complex
222+
# CG coefficient from: (m1, m2, m) -> (m, m2, m1)
223+
first_step = _dispatch.permute(complex_cg, [2, 1, 0]) @ r2c[l1]
224+
# The result `first_step` has shape (2*L+1, 2*l2+1, 2*l1+1)
225+
#
226+
# 2) take the matrix product:
227+
# \sum_{m2'} ...
228+
# ... (first_step)_{m',m1,m2'}^{l,l1} U^{\dagger}_{m2'm2}^{l2}
229+
first_step_swap = first_step.swapaxes(1, 2)
230+
# The result `first_step_swap` has shape (2*L+1, 2*l1+1, 2*l2+1)
231+
second_step = first_step_swap @ r2c[l2]
232+
# The result `second_step` has shape (2*L+1, 2*l1+1, 2*l2+1)
233+
#
234+
# 3) take the matrix product:
235+
# \sum_{m'} U_{mm'}^{l} (second_step)_{m',m1,m2}^{l,l1,l2}
236+
second_step_swap = _dispatch.permute(second_step, [1, 0, 2])
237+
# The result `second_step_swap` has shape (2*l1+1, 2*L+1, 2*l2+1)
238+
third_step = c2r[o3_lambda] @ second_step_swap
239+
# The result `third_step` has shape (2*l1+1, 2*L+1, 2*l2+1)
240+
third_step_swap = _dispatch.permute(third_step, [0, 2, 1])
241+
# The result `third_step_swap` has shape (2*l1+1, 2*l2+1, 2*L+1)
212242

213243
if (l1 + l2 + o3_lambda) % 2 == 0:
214-
cg_l1l2lam_dense = _dispatch.real(real_cg)
244+
cg_l1l2lam_dense = _dispatch.real(third_step_swap)
215245
else:
216-
cg_l1l2lam_dense = _dispatch.imag(real_cg)
246+
cg_l1l2lam_dense = _dispatch.imag(third_step_swap)
217247

218248
coeff_dict[(l1, l2, o3_lambda)] = _dispatch.to(
219249
cg_l1l2lam_dense,
@@ -366,7 +396,7 @@ def _real2complex(o3_lambda: int, like: Array) -> Array:
366396
# Positive part
367397
result[o3_lambda + m, o3_lambda + m] = inv_sqrt_2 * ((-1) ** m)
368398

369-
return result
399+
return result.T
370400

371401

372402
def _complex2real(o3_lambda: int, like) -> Array:
@@ -394,10 +424,10 @@ def cg_couple(
394424
Go from an uncoupled product basis that behave like a product of spherical harmonics
395425
to a coupled basis that behaves like a single spherical harmonic.
396426
397-
The ``array`` shape should be ``(n_samples, 2 * l1 + 1, 2 * l2 + 1, n_q)``.
398-
``n_samples`` is the number of samples, and ``n_q`` is the number of properties.
427+
The ``array`` shape should be ``(n_s, 2 * l1 + 1, 2 * l2 + 1, n_q)``.
428+
``n_s`` is the number of samples, and ``n_q`` is the number of properties.
399429
400-
This function will output a list of arrays, whose shape will be ``[n_samples, (2 *
430+
This function will output a list of arrays, whose shape will be ``[n_s, (2 *
401431
o3_lambda+1), n_q]``, with the requested ``o3_lambdas``.
402432
403433
These arrays will contain the result of projecting from a product of spherical
@@ -411,7 +441,7 @@ def cg_couple(
411441
The operation is dispatched such that numpy arrays or torch tensors are
412442
automatically handled.
413443
414-
:param array: input array with shape ``[n_samples, 2 * l1 + 1, 2 * l2 + 1, n_q]``
444+
:param array: input array with shape ``[n_s, 2 * l1 + 1, 2 * l2 + 1, n_q]``
415445
:param o3_lambdas: list of degrees of spherical harmonics to compute
416446
:param cg_coefficients: CG coefficients as returned by
417447
:py:func:`calculate_cg_coefficients` with the same ``cg_backed`` given to this
@@ -438,18 +468,24 @@ def cg_couple(
438468
for o3_lambda in o3_lambdas
439469
]
440470
elif cg_backend == "python-dense":
441-
results = []
442-
443471
n_samples = array.shape[0]
444472
n_properties = array.shape[3]
445473

446-
array = array.swapaxes(1, 3)
447-
array = array.reshape(n_samples * n_properties, 2 * l2 + 1, 2 * l1 + 1)
474+
# Move properties next to samples
475+
array = _dispatch.permute(array, [0, 3, 1, 2])
448476

477+
array = array.reshape(n_samples * n_properties, 2 * l1 + 1, 2 * l2 + 1)
478+
479+
results = []
449480
for o3_lambda in o3_lambdas:
450481
result = _cg_couple_dense(array, o3_lambda, cg_coefficients)
482+
483+
# Separate samples and properties
451484
result = result.reshape(n_samples, n_properties, -1)
485+
486+
# Back to TensorBlock-like axes
452487
result = result.swapaxes(1, 2)
488+
453489
results.append(result)
454490

455491
return results
@@ -499,7 +535,11 @@ def _cg_couple_sparse(
499535
m2 = int(m1m2mu[1])
500536
mu = int(m1m2mu[2])
501537
# Broadcast arrays, multiply together and with CG coeff
502-
output[mu, :, :] += arrays[str((m1, m2))] * cg_l1l2lam.values[i, 0]
538+
output[mu, :, :] += (
539+
arrays[str((m1, m2))]
540+
* (-1) ** (l1 + l2 + o3_lambda)
541+
* cg_l1l2lam.values[i, 0]
542+
)
503543

504544
return output.swapaxes(0, 1)
505545

@@ -514,26 +554,23 @@ def _cg_couple_dense(
514554
degree ``o3_lambda``) using CG coefficients. This is a "dense" implementation, using
515555
all CG coefficients at the same time.
516556
517-
:param array: input array, we expect a shape of ``[samples, 2*l1 + 1, 2*l2 + 1]``
557+
:param array: input array, we expect a shape of ``[N, 2*l1 + 1, 2*l2 + 1]``
518558
:param o3_lambda: value of lambda for the output spherical harmonic
519559
:param cg_coefficients: CG coefficients as returned by
520560
:py:func:`calculate_cg_coefficients` with ``cg_backed="python-dense"``
561+
562+
:return: array of shape ``[N, 2 * o3_lambda + 1]``
521563
"""
522564
assert len(array.shape) == 3
523565

524566
l1 = (array.shape[1] - 1) // 2
525567
l2 = (array.shape[2] - 1) // 2
526568

527-
cg_l1l2lam = cg_coefficients.block({"l1": l1, "l2": l2, "lambda": o3_lambda}).values
528-
529-
# [samples, l1, l2] => [samples, (l1 l2)]
530-
array = array.reshape(-1, (2 * l1 + 1) * (2 * l2 + 1))
531-
532-
# [l1, l2, lambda] -> [(l1 l2), lambda]
533-
cg_l1l2lam = cg_l1l2lam.reshape(-1, 2 * o3_lambda + 1)
569+
cg_l1l2lam = (-1) ** (l1 + l2 + o3_lambda) * cg_coefficients.block(
570+
{"l1": l1, "l2": l2, "lambda": o3_lambda}
571+
).values
534572

535-
# [samples, (l1 l2)] @ [(l1 l2), lambda] => [samples, lambda]
536-
return array @ cg_l1l2lam
573+
return _dispatch.tensordot(array, cg_l1l2lam[0, ..., 0], axes=([2, 1], [1, 0]))
537574

538575

539576
# ======================================================================= #

python/featomic/featomic/clebsch_gordan/_dispatch.py

+28
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,34 @@ def concatenate(arrays: List[TorchTensor], axis: int):
6363
raise TypeError(UNKNOWN_ARRAY_TYPE)
6464

6565

66+
def permute(array, axes: List[int]):
67+
"""
68+
Permute the axes of the given ``array``. For numpy and torch, ``transpose`` and
69+
``permute`` are equivalent operations.
70+
"""
71+
if isinstance(array, TorchTensor):
72+
return torch.permute(array, axes)
73+
elif isinstance(array, np.ndarray):
74+
return np.transpose(array, axes)
75+
else:
76+
raise TypeError(UNKNOWN_ARRAY_TYPE)
77+
78+
79+
def tensordot(array_1, array_2, axes: List[List[int]]):
80+
"""
81+
Compute tensor dot product along specified axes for arrays.
82+
83+
This function has the same behavior as ``np.tensordot(array_1, array_2, axes)`` and
84+
``torch.tensordot(array_1, array_2, axes)``.
85+
"""
86+
if isinstance(array_1, TorchTensor):
87+
return torch.tensordot(array_1, array_2, axes)
88+
elif isinstance(array_1, np.ndarray):
89+
return np.tensordot(array_1, array_2, axes)
90+
else:
91+
raise TypeError(UNKNOWN_ARRAY_TYPE)
92+
93+
6694
def empty_like(array, shape: Optional[List[int]] = None, requires_grad: bool = False):
6795
"""
6896
Create an uninitialized array, with the given ``shape``, and similar dtype, device

0 commit comments

Comments
 (0)