.. _Spherical Harmonics: Spherical Harmonics =================== The spherical harmonics :math:`Y^l(x)` are functions defined on the sphere :math:`S^2`. They form a basis of the space on function on the sphere: .. math:: \mathcal{F} = \{ S^2 \longrightarrow \mathbb{R} \} On this space it is natural how the group :math:`O(3)` acts, Given :math:`p_a, p_v` two scalar representations: .. math:: [L(g) f](x) = p_v(g) f(p_a(g) R(g)^{-1} x), \quad \forall f \in \mathcal{F}, x \in S^2 :math:`L` is representation of :math:`O(3)`. But :math:`L` is not irreducible. It can be decomposed via a change of basis into a sum of irreps, In a handwavey notation we can write: .. math:: Y^T L(g) Y = 0 \oplus 1 \oplus 2 \oplus 3 \oplus \dots where the change of basis are the spherical harmonics! This notation is handwavey because :math:`x` is a continuous variable, and therefore the change of basis :math:`Y` is not a matrix. As a consequence, the spherical harmonics are equivariant, .. math:: Y^l(R(g) x) = D^l(g) Y^l(x) .. jupyter-execute:: :hide-code: import torch import math from e3nn import o3 import plotly.graph_objects as go axis = dict( showbackground=False, showticklabels=False, showgrid=False, zeroline=False, title='', nticks=3, ) layout = dict( width=690, height=160, scene=dict( xaxis=dict( **axis, range=[-8, 8] ), yaxis=dict( **axis, range=[-2, 2] ), zaxis=dict( **axis, range=[-2, 2] ), aspectmode='manual', aspectratio=dict(x=8, y=2, z=2), camera=dict( up=dict(x=0, y=0, z=1), center=dict(x=0, y=0, z=0), eye=dict(x=0, y=-5, z=5), projection=dict(type='orthographic'), ), ), paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", margin=dict(l=0, r=0, t=0, b=0) ) cmap_bwr = [[0, 'rgb(0,50,255)'], [0.5, 'rgb(200,200,200)'], [1, 'rgb(255,50,0)']] def s2_grid(): betas = torch.linspace(0, math.pi, 40) alphas = torch.linspace(0, 2 * math.pi, 80) beta, alpha = torch.meshgrid(betas, alphas, indexing='ij') return o3.angles_to_xyz(alpha, beta) def trace(r, f, c, radial_abs: bool = True): if radial_abs: a = f.abs() else: a = 1 return dict( x=a * r[..., 0] + c[0], y=a * r[..., 1] + c[1], z=a * r[..., 2] + c[2], surfacecolor=f ) def plot(data, radial_abs: bool = True): r = s2_grid() n = data.shape[-1] traces = [ trace(r, data[..., i], torch.tensor([2.0 * i - (n - 1.0), 0.0, 0.0]), radial_abs=radial_abs) for i in range(n) ] cmax = max(d['surfacecolor'].abs().max().item() for d in traces) traces = [go.Surface(**d, colorscale=cmap_bwr, cmin=-cmax, cmax=cmax) for d in traces] fig = go.Figure(data=traces, layout=layout) fig.show() .. jupyter-execute:: r = s2_grid() ``r`` is a grid on the sphere. .. jupyter-execute:: :hide-code: fig = go.Figure( data=[ go.Scatter3d( x=r[..., 0].flatten(), y=r[..., 1].flatten(), z=r[..., 2].flatten(), mode='markers', marker=dict( size=1, ), ) ], layout=dict( width=500, height=300, scene=dict( xaxis=dict( **axis, range=[-1, 1] ), yaxis=dict( **axis, range=[-1, 1] ), zaxis=dict( **axis, range=[-1, 1] ), aspectmode='manual', aspectratio=dict(x=3, y=3, z=3), camera=dict( up=dict(x=0, y=0, z=1), center=dict(x=0, y=0, z=0), eye=dict(x=0, y=-5, z=5), projection=dict(type='orthographic'), ), ), paper_bgcolor="rgba(0,0,0,0)", plot_bgcolor="rgba(0,0,0,0)", margin=dict(l=0, r=0, t=0, b=0) ) ) fig.show() Each point on the sphere has 3 components. If we plot the value of each of the 3 component separately we obtain the following figure: .. jupyter-execute:: plot(r, radial_abs=False) x, y and z are represented as 3 scalar fields on 3 different spheres. To obtain a nicer figure (that looks like the spherical harmonics shown on Wikipedia) we can deform the spheres into a shape that has its radius equal to the absolute value of the plotted quantity: .. jupyter-execute:: plot(r) :math:`Y^1` is the identity function. Now let's compute :math:`Y^2`, for this we take the tensor product :math:`r \otimes r` and extract the :math:`L=2` part of it. .. jupyter-execute:: tp = o3.ElementwiseTensorProduct("1o", "1o", ['2e'], irrep_normalization='norm') y2 = tp(r, r) plot(y2) Similarly, the next spherical harmonic function :math:`Y^3` is the :math:`L=3` part of :math:`r \otimes r \otimes r`: .. jupyter-execute:: tp = o3.ElementwiseTensorProduct("2e", "1o", ['3o'], irrep_normalization='norm') y3 = tp(y2, r) plot(y3) The functions below are more efficient versions not using `e3nn.o3.ElementwiseTensorProduct`: .. rubric:: Details .. autofunction:: e3nn.o3.spherical_harmonics .. autofunction:: e3nn.o3.spherical_harmonics_alpha_beta .. autofunction:: e3nn.o3.Legendre