Source code for wigner3j

"""The code in this submodule is taken directly from the MIT-licensed
'spherical' package. Below is the original license text:

Copyright (c) 2021 Mike Boyle

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""

import math

import numpy as np


[docs] def A(j, j2, j3, m1): return math.sqrt( (j**2 - (j2 - j3) ** 2) * ((j2 + j3 + 1) ** 2 - j**2) * (j**2 - m1**2) )
[docs] def B(j, j2, j3, m2, m3): return (2 * j + 1) * ( (m2 + m3) * (j2 * (j2 + 1) - j3 * (j3 + 1)) - (m2 - m3) * j * (j + 1) )
[docs] def Xf(j, j2, j3, m1): return j * A(j + 1, j2, j3, m1)
[docs] Yf = B
[docs] def Zf(j, j2, j3, m1): return (j + 1) * A(j, j2, j3, m1)
[docs] def normalize(f, j_min, j_max): norm = 0.0 for j in range(j_min, j_max + 1): norm += (2 * j + 1) * f[j] ** 2 f[j_min : j_max + 1] /= math.sqrt(norm)
[docs] def determine_signs(f, j_min, j_max, j2, j3, m2, m3): if (f[j_max] < 0.0 and (-1) ** (j2 - j3 + m2 + m3) > 0) or ( f[j_max] > 0.0 and (-1) ** (j2 - j3 + m2 + m3) < 0 ): f[j_min : j_max + 1] *= -1.0
[docs] class Wigner3jCalculator: def __init__(self, j2_max, j3_max): self._size = j2_max + j3_max + 1
[docs] self.workspace = np.zeros(4 * self._size, dtype=np.float64)
@property
[docs] def size(self): return self._size
[docs] def calculate(self, j2, j3, m2, m3): # Compute Wigner 3-j symbols # # For given values of j₂, j₃, m₂, m₃, this computes all valid values of # # ⎛j₁ j₂ j₃⎞ _ ⎛j₂ j₃ j₁⎞ _ ⎛j₃ j₁ j₂⎞ # ⎝m₁ m₂ m₃⎠ - ⎝m₂ m₃ m₁⎠ - ⎝m₃ m₁ m₂⎠ # # The valid values have m₁=-m₂-m₃ and j₁ ranging from max(|j₂-j₃|, |m₁|) to # j₂+j₃. # The calculation uses the approach described by Luscombe and Luban (1998) # <https://doi.org/10.1103/PhysRevE.57.7274>, which is a recurrence method, # leading to significant gains in speed and accuracy. # # The returned array is a slice of this object's `workspace` array, and so will # not # remain the same between calls to this function. If you want to keep a copy of # the results, explicitly call the `numpy.copy` method. # # The returned array is indexed by j₁. In particular, note that some invalid # j₁ indices are accessible, but have value 0.0. # # This implementation uses several tricks gleaned from the Fortran code in # <https://github.com/SHTOOLS/SHTOOLS>, which also implements the Luscombe-Luban # algorithm. In particular, that code (and now this code) treats several special # cases that were not clearly specified by Luscombe-Luban. # # To use this object, do something like this: # # ..code-block:: python # # # Try to do this just once because it allocates memory, which is slow # calculator = Wigner3jCalculator(j2_max, j3_max) # # # Presumably, the following is inside some loop over j2, j3, m2, m3 # w3j = calculator.calculate(j2, j3, m2, m3) # m1 = - m2 - m3 # for j1 in range(max(abs(j2-j3), abs(m1)), j2+j3+1): # w3j[j1] # This is the value of the 3-j symbol written above # # Again, the array w3j contains accessible memory outside of the bounds of j1 # given in the above loop, but those values will all be 0.0. m1 = -(m2 + m3) undefined_min = False undefined_max = False scale_factor = 1_000.0 # Set up the workspace self.workspace[:] = 0.0 f = self.workspace[: self.size] sf = self.workspace[self.size : 2 * self.size] rf = sf # An alias for the same memory as `sf` F_minus = self.workspace[2 * self.size : 3 * self.size] F_plus = self.workspace[3 * self.size : 4 * self.size] # Calculate some useful bounds j_min = max(abs(j2 - j3), abs(m2 + m3)) j_max = j2 + j3 # Skip all calculations if there are no nonzero elements if abs(m2) > j2 or abs(m3) > j3: return f if j_max < j_min: return f # When only one term is present, we have a simple formula if j_max == j_min: f[j_min] = 1.0 / math.sqrt(2.0 * j_min + 1.0) if (f[j_min] < 0.0 and (-1) ** (j2 - j3 + m2 + m3) > 0) or ( f[j_min] > 0.0 and (-1) ** (j2 - j3 + m2 + m3) < 0 ): f[j_min] *= -1.0 return f ########################################################################## # Forward iteration over first "nonclassical" region j_min ≤ j ≤ j_minus # ########################################################################## Xf_j_min = Xf(j_min, j2, j3, m1) Yf_j_min = Yf(j_min, j2, j3, m2, m3) if m1 == 0 and m2 == 0 and m3 == 0: # Recurrence is undefined, but it's okay because all odd terms must be zero F_minus[j_min] = 1.0 F_minus[j_min + 1] = 0.0 j_minus = j_min + 1 elif Yf_j_min == 0.0: # The second term is either undefined or zero if Xf_j_min == 0.0: undefined_min = True j_minus = j_min else: F_minus[j_min] = 1.0 F_minus[j_min + 1] = 0.0 j_minus = j_min + 1 elif Xf_j_min * Yf_j_min >= 0.0: # The second term is already in the classical region F_minus[j_min] = 1.0 F_minus[j_min + 1] = -Yf_j_min / Xf_j_min j_minus = j_min + 1 else: # Use recurrence relation, Eq. (3) from Luscombe and Luban (1998) sf[j_min] = -Xf_j_min / Yf_j_min j_minus = j_max for j in range(j_min + 1, j_max): denominator = Yf(j, j2, j3, m2, m3) + Zf(j, j2, j3, m1) * sf[j - 1] Xf_j = Xf(j, j2, j3, m1) if ( abs(Xf_j) > abs(denominator) or Xf_j * denominator >= 0.0 or denominator == 0.0 ): j_minus = j - 1 break else: sf[j] = -Xf_j / denominator F_minus[j_minus] = 1.0 for k in range(1, j_minus - j_min + 1): F_minus[j_minus - k] = F_minus[j_minus - k + 1] * sf[j_minus - k] if j_minus == j_min: # Calculate at least two terms so that these can be used in three-term # recursion F_minus[j_min + 1] = -Yf_j_min / Xf_j_min j_minus = j_min + 1 if j_minus == j_max: # We're finished! f[j_min : j_max + 1] = F_minus[j_min : j_max + 1] normalize(f, j_min, j_max) determine_signs(f, j_min, j_max, j2, j3, m2, m3) return f ########################################################################## # Reverse iteration over second "nonclassical" region j_plus ≤ j ≤ j_max # ########################################################################## Yf_j_max = Yf(j_max, j2, j3, m2, m3) Zf_j_max = Zf(j_max, j2, j3, m1) if m1 == 0 and m2 == 0 and m3 == 0: # Recurrence is undefined, but it's okay because all odd terms must be zero F_plus[j_max] = 1.0 F_plus[j_max - 1] = 0.0 j_plus = j_max - 1 elif Yf_j_max == 0.0: # The second term is either undefined or zero if Zf_j_max == 0.0: undefined_max = True j_plus = j_max else: F_plus[j_max] = 1.0 F_plus[j_max - 1] = -Yf_j_max / Zf_j_max j_plus = j_max - 1 elif Yf_j_max * Zf_j_max >= 0.0: # The second term is already in the classical region F_plus[j_max] = 1.0 F_plus[j_max - 1] = -Yf_j_max / Zf_j_max j_plus = j_max - 1 else: # Use recurrence relation, Eq. (2) from Luscombe and Luban (1998) rf[j_max] = -Zf_j_max / Yf_j_max j_plus = j_min for j in range(j_max - 1, j_minus - 1, -1): denominator = Yf(j, j2, j3, m2, m3) + Xf(j, j2, j3, m1) * rf[j + 1] Zf_j = Zf(j, j2, j3, m1) if ( denominator == 0.0 or abs(Zf_j) > abs(denominator) or Zf_j * denominator >= 0.0 ): j_plus = j + 1 break else: rf[j] = -Zf_j / denominator F_plus[j_plus] = 1.0 for k in range(1, j_max - j_plus + 1): F_plus[j_plus + k] = F_plus[j_plus + k - 1] * rf[j_plus + k] if j_plus == j_max: F_plus[j_max - 1] = -Yf_j_max / Zf_j_max j_plus = j_max - 1 ###################################################################### # Three-term recurrence over "classical" region j_minus ≤ j ≤ j_plus # ###################################################################### if undefined_min and undefined_max: raise ValueError( "Cannot initialize recurrence in Wigner3jCalculator.calculate" ) if not undefined_min and not undefined_max: # Iterate upwards and downwards, meeting in the middle j_mid = (j_minus + j_plus) // 2 for j in range(j_minus, j_mid): F_minus[j + 1] = -( Yf(j, j2, j3, m2, m3) * F_minus[j] + Zf(j, j2, j3, m1) * F_minus[j - 1] ) / Xf(j, j2, j3, m1) if abs(F_minus[j + 1]) > 1.0: F_minus[j_min : j + 1 + 1] /= scale_factor if abs(F_minus[j + 1] / F_minus[j - 1]) < 1.0 and F_minus[j + 1] != 0.0: j_mid = j + 1 break F_minus_j_mid = F_minus[j_mid] if ( F_minus[j_mid - 1] != 0.0 and abs(F_minus_j_mid / F_minus[j_mid - 1]) < 1.0e-6 ): j_mid -= 1 F_minus_j_mid = F_minus[j_mid] for j in range(j_plus, j_mid, -1): F_plus[j - 1] = -( Xf(j, j2, j3, m1) * F_plus[j + 1] + Yf(j, j2, j3, m2, m3) * F_plus[j] ) / Zf(j, j2, j3, m1) if abs(F_plus[j - 1]) > 1.0: F_plus[j - 1 : j_max + 1] /= scale_factor F_plus_j_mid = F_plus[j_mid] if j_mid == j_max: f[j_min : j_max + 1] = F_minus[j_min : j_max + 1] elif j_mid == j_min: f[j_min : j_max + 1] = F_plus[j_min : j_max + 1] else: f[j_min : j_mid + 1] = ( F_minus[j_min : j_mid + 1] * F_plus_j_mid / F_minus_j_mid ) f[j_mid + 1 : j_max + 1] = F_plus[j_mid + 1 : j_max + 1] elif not undefined_min and undefined_max: # Iterate upwards only for j in range(j_minus, j_plus): F_minus[j + 1] = -( Zf(j, j2, j3, m1) * F_minus[j - 1] + Yf(j, j2, j3, m2, m3) * F_minus[j] ) / Xf(j, j2, j3, m1) if abs(F_minus[j + 1]) > 1: F_minus[j_min : j + 1 + 1] /= scale_factor f[j_min : j_max + 1] = F_minus[j_min : j_max + 1] elif undefined_min and not undefined_max: # Iterate downwards only for j in range(j_plus, j_min, -1): F_plus[j - 1] = -( Xf(j, j2, j3, m1) * F_plus[j + 1] + Yf(j, j2, j3, m2, m3) * F_plus[j] ) / Zf(j, j2, j3, m1) if abs(F_plus[j - 1]) > 1: F_plus[j - 1 : j_max + 1] /= scale_factor f[j_min : j_max + 1] = F_plus[j_min : j_max + 1] ############# # Finish up # ############# normalize(f, j_min, j_max) determine_signs(f, j_min, j_max, j2, j3, m2, m3) return f
[docs] def Wigner3j(j_1, j_2, j_3, m_1, m_2, m_3): if m_1 + m_2 + m_3 != 0: return 0.0 if abs(m_1) > j_1 or abs(m_2) > j_2 or abs(m_3) > j_3: return 0.0 # Permute cyclically to ensure that j_1 is the largest if j_1 == max(j_1, j_2, j_3): pass elif j_2 == max(j_1, j_2, j_3): j_1, j_2, j_3 = j_2, j_3, j_1 m_1, m_2, m_3 = m_2, m_3, m_1 else: # j_3 == max(j_1, j_2, j_3) j_1, j_2, j_3 = j_3, j_1, j_2 m_1, m_2, m_3 = m_3, m_1, m_2 if j_1 > j_2 + j_3: return 0.0 calculator = Wigner3jCalculator(j_2, j_3) w3j = calculator.calculate(j_2, j_3, m_2, m_3) return w3j[j_1]
[docs] def clebsch_gordan(j_1, m_1, j_2, m_2, j_3, m_3): return ( (-1.0) ** (j_1 - j_2 + m_3) * math.sqrt(2 * j_3 + 1) * Wigner3j(j_1, j_2, j_3, m_1, m_2, -m_3) )