@@ -197,23 +197,53 @@ def _build_dense_cg_coeff_dict(
197
197
device = device ,
198
198
dtype = complex_like .dtype ,
199
199
)
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)
212
242
213
243
if (l1 + l2 + o3_lambda ) % 2 == 0 :
214
- cg_l1l2lam_dense = _dispatch .real (real_cg )
244
+ cg_l1l2lam_dense = _dispatch .real (third_step_swap )
215
245
else :
216
- cg_l1l2lam_dense = _dispatch .imag (real_cg )
246
+ cg_l1l2lam_dense = _dispatch .imag (third_step_swap )
217
247
218
248
coeff_dict [(l1 , l2 , o3_lambda )] = _dispatch .to (
219
249
cg_l1l2lam_dense ,
@@ -366,7 +396,7 @@ def _real2complex(o3_lambda: int, like: Array) -> Array:
366
396
# Positive part
367
397
result [o3_lambda + m , o3_lambda + m ] = inv_sqrt_2 * ((- 1 ) ** m )
368
398
369
- return result
399
+ return result . T
370
400
371
401
372
402
def _complex2real (o3_lambda : int , like ) -> Array :
@@ -394,10 +424,10 @@ def cg_couple(
394
424
Go from an uncoupled product basis that behave like a product of spherical harmonics
395
425
to a coupled basis that behaves like a single spherical harmonic.
396
426
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.
399
429
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 *
401
431
o3_lambda+1), n_q]``, with the requested ``o3_lambdas``.
402
432
403
433
These arrays will contain the result of projecting from a product of spherical
@@ -411,7 +441,7 @@ def cg_couple(
411
441
The operation is dispatched such that numpy arrays or torch tensors are
412
442
automatically handled.
413
443
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]``
415
445
:param o3_lambdas: list of degrees of spherical harmonics to compute
416
446
:param cg_coefficients: CG coefficients as returned by
417
447
:py:func:`calculate_cg_coefficients` with the same ``cg_backed`` given to this
@@ -438,18 +468,24 @@ def cg_couple(
438
468
for o3_lambda in o3_lambdas
439
469
]
440
470
elif cg_backend == "python-dense" :
441
- results = []
442
-
443
471
n_samples = array .shape [0 ]
444
472
n_properties = array .shape [3 ]
445
473
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 ] )
448
476
477
+ array = array .reshape (n_samples * n_properties , 2 * l1 + 1 , 2 * l2 + 1 )
478
+
479
+ results = []
449
480
for o3_lambda in o3_lambdas :
450
481
result = _cg_couple_dense (array , o3_lambda , cg_coefficients )
482
+
483
+ # Separate samples and properties
451
484
result = result .reshape (n_samples , n_properties , - 1 )
485
+
486
+ # Back to TensorBlock-like axes
452
487
result = result .swapaxes (1 , 2 )
488
+
453
489
results .append (result )
454
490
455
491
return results
@@ -499,7 +535,11 @@ def _cg_couple_sparse(
499
535
m2 = int (m1m2mu [1 ])
500
536
mu = int (m1m2mu [2 ])
501
537
# 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
+ )
503
543
504
544
return output .swapaxes (0 , 1 )
505
545
@@ -514,26 +554,23 @@ def _cg_couple_dense(
514
554
degree ``o3_lambda``) using CG coefficients. This is a "dense" implementation, using
515
555
all CG coefficients at the same time.
516
556
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]``
518
558
:param o3_lambda: value of lambda for the output spherical harmonic
519
559
:param cg_coefficients: CG coefficients as returned by
520
560
:py:func:`calculate_cg_coefficients` with ``cg_backed="python-dense"``
561
+
562
+ :return: array of shape ``[N, 2 * o3_lambda + 1]``
521
563
"""
522
564
assert len (array .shape ) == 3
523
565
524
566
l1 = (array .shape [1 ] - 1 ) // 2
525
567
l2 = (array .shape [2 ] - 1 ) // 2
526
568
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
534
572
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 ]))
537
574
538
575
539
576
# ======================================================================= #
0 commit comments