Spherical harmonic transforms¶
Spherical harmonic transforms for MW and symmetric MW samplings are created via the
factory function harmonic_transform. The factory function takes
at least one parameter, lmax.
from tensossht import harmonic_transform
transform = harmonic_transform(lmax=8, spin=0, dtype=tf.float64)
Optionally, we can also specify lmin, mmin, and mmax, though little testing
has been done apart from mmin=0. Finally, the spin can be specified either with
spin=0 for single-spin transforms, or with smin=-1 and smax=1 for multi-spin
transforms. The latter case is another way to create a transform for real image-space
signals. for this specific case:
rtransform = harmonic_transform(lmax=8, mmin=0, spin=0, dtype=tf.float64)
The underlying floating point can be specified via the dtype keyword argument. It
defaults to tf.float32 (just like any other tensorflow operation). Furthermore, the
image-space sampling scheme can be specified via the sampling keyword. It defaults
to “MW”.
MW Sampling in harmonic-space and image-space¶
The transform works on a fixed basis in harmonic space, fully specifid by lmax,
lmin, mmax, mmin. The order of the basis functions can be assessed via the
two properties:
>>> transform.llabels
<tf.Tensor: shape=(64,), dtype=int32, numpy=
array([0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4,
4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6,
6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7],
dtype=int32)>
>>> transform.mlabels
<tf.Tensor: shape=(64,), dtype=int32, numpy=
array([ 0, -1, 0, 1, -2, -1, 0, 1, 2, -3, -2, -1, 0, 1, 2, 3, -4,
-3, -2, -1, 0, 1, 2, 3, 4, -5, -4, -3, -2, -1, 0, 1, 2, 3,
4, 5, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, -7, -6,
-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7], dtype=int32)>
The transform for real signals only contains \(m \geq 0\) components:
>>> rtransform.mlabels
<tf.Tensor: shape=(36,), dtype=int32, numpy=
array([0, 0, 1, 0, 1, 2, 0, 1, 2, 3, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 5, 0,
1, 2, 3, 4, 5, 6, 0, 1, 2, 3, 4, 5, 6, 7], dtype=int32)>
The sampling can be queried for \(\theta\) and \(\phi\):
>>> transform.thetas.numpy().round(3)
array([0.209, 0.628, 1.047, 1.466, 1.885, 2.304, 2.723, 3.142])
>>> transform.phis.numpy().round(3)
array([0. , 0.419, 0.838, 1.257, 1.676, 2.094, 2.513, 2.932, 3.351,
3.77 , 4.189, 4.608, 5.027, 5.445, 5.864])
A \(\theta\) by \(\phi\) grid is also provided for convenience:
>>> transform.grid.shape
TensorShape([2, 8, 15])
As well as list of \((\theta, \phi)\) points:
>>> transform.points.shape
TensorShape([120, 2])
Forward and Inverse Transforms with MW Sampling¶
The transforms themselves are called via
forward() and
inverse(). For instance, we can create a
set of two random images and transform them back and forth.
from pytest import approx
nthetas = transform.lmax
nphis = 2 * transform.lmax - 1
assert (int(nthetas), int(nphis)) == transform.sampling.grid.shape[1:]
images = tf.complex(
tf.random.uniform((2, nthetas, nphis), dtype=transform.real_dtype),
tf.random.uniform((2, nthetas, nphis), dtype=transform.real_dtype),
)
representable_images = transform.inverse(transform.forward(images))
assert tf.reduce_any(representable_images != 0)
A first back-and-forth removes from the images components that cannot be represented with the transform’s harmonic-space basis. The second back-and-forth, however, should yield exactly the same result as the first, except for numerical noise.
toandfro = transform.inverse(transform.forward(representable_images))
assert toandfro.numpy() == approx(representable_images.numpy())
Similarly, for a real image-space signal:
images = tf.random.uniform((2, nthetas, nphis), dtype=transform.real_dtype)
representable_images = rtransform.inverse(rtransform.forward(images))
toandfro = rtransform.inverse(rtransform.forward(representable_images))
assert toandfro.numpy() == approx(representable_images.numpy())
Note that both
forward() and
inverse() accept
keywords specifying which dimension corresponds to \(\theta\),
\(\phi\) and to the harmonic space coefficients. By default, and to avoid
extra transpose operations, \(\theta\) and the harmonic-space coefficients
should be stored along the last dimension, and \(\phi\) on the penultimate.
Forward and Inverse Transforms with MWSS Sampling¶
MW symmetric sampling is a more widespread sampling for 360 images. It requires slightly more memory and the transforms require slightly more compute. However, both samplings are quite similar. A transform with MWSS sampling can be created as follows:
from tensossht import harmonic_transform
transform = harmonic_transform(lmax=5, spin=0, sampling="MWSS", dtype=tf.float64)
The sampling scheme is available through the transform:
>>> transform.sampling.thetas.numpy().round(3)
array([0. , 0.628, 1.257, 1.885, 2.513, 3.142])
>>> transform.sampling.phis.numpy().round(3)
array([0. , 0.628, 1.257, 1.885, 2.513, 3.142, 3.77 , 4.398, 5.027,
5.655])
Let’s create a full set of spherical harmonics with MWSS sampling. For illustration purposes, the \(\phi\) and \(\theta\) dimensions are ordered in a less orthodox (and less efficient) manner:
from tensossht import spherical_harmonics
grid = transform.sampling.grid
sph = spherical_harmonics(grid[0], grid[1], lmax=transform.lmax)
sph is a three-dimensional array with the first dimension refering to \(theta\),
the second to \(phi\), and the second dimension to the spherical harmonics with
\(l \leq 4\).
>>> sph.shape
TensorShape([6, 10, 25])
In spherical-harmonic space, we expect the set of images to be transformed to the identity matrix, since each image is a single spherical harmonic:
from pytest import approx
sph_space = transform.forward(sph, theta_dim=0, phi_dim=1).numpy()
assert sph_space == approx(tf.eye(sph.shape[-1]).numpy(), abs=1e-7)
Similarly, for a real image-space signal:
rsph = tf.math.real(sph)
rtransform = transform.real
rsph_space = rtransform.forward(rsph, theta_dim=0, phi_dim=1)
as_complex = transform.forward(
tf.complex(rsph, tf.constant(0, rtransform.real_dtype)),
theta_dim=0,
phi_dim=1,
)
as_complex = tf.transpose(
tf.boolean_mask(tf.transpose(as_complex), transform.mlabels >= 0)
)
assert rsph_space.numpy() == approx(as_complex.numpy())
We can try the same operations for inverse transforms. If we feed the inverse transform an identity matrix, it should spew out the spherical harmonics under the MWSS sampling scheme:
coefficients = tf.eye(
transform.lmax * transform.lmax, dtype=transform.complex_dtype
)
real_space = transform.inverse(coefficients, phi_dim=1, theta_dim=0)
assert real_space.numpy() == approx(sph.numpy(), abs=1e-6)
Similarly for real-space signals:
coefficients = (
tf.eye(rtransform.labels.shape[1], dtype=rtransform.complex_dtype)
* tf.cast(
tf.where(rtransform.mlabels == 0, 1.0, 0.5),
dtype=rtransform.complex_dtype
)
)
real_space = rtransform.inverse(coefficients, phi_dim=1, theta_dim=0)
rsph = spherical_harmonics(grid[0], grid[1], lmax=rtransform.lmax, mmin=0)
assert real_space.numpy() == approx(tf.math.real(rsph).numpy(), abs=1e-5, rel=1e-5)
API¶
- class tensossht.transforms.transforms.HarmonicTransform(forward, inverse, image_sampling, harmonic_sampling)[source]¶
Fast transform between image space and spherical harmonic space.
- Parameters:
forward (Callable) –
inverse (Callable) –
image_sampling (ImageSamplingBase) –
harmonic_sampling (HarmonicSampling) –
- forward(images: tf.Tensor, theta_dim: int = -2, phi_dim: int = -1, coeff_dim: int = -1) tf.Tensor¶
Transform from image to harmonic space. The image axes
theta_dimandphi_dim, as well as the harmonic-coefficient axiscoeff_dimcan be specified on input. They default to the innermost axes (.i.e. \(\phi\) is contiguous in memory).
- inverse(images: tf.Tensor, theta_dim: int = -2, phi_dim: int = -1, coeff_dim: int = -1) tf.Tensor¶
Transform from harmonic to image space. The image axes
theta_dimandphi_dim, as well as the harmonic-coefficient axiscoeff_dimcan be specified on input. They default to the innermost axes (.i.e. \(\phi\) is contiguous in memory).
- property complex¶
Transform for complex signals.
If the transform is already for complex signals (\(m_\mathrm{min} \leq 0\)), then this property returns self. Otherwise it returns an equivalent transform with \(m_\mathrm{min} = -l_\mathrm{max}\).
- property complex_dtype¶
- property grid¶
- harmonic_sampling¶
Harmonic-space sampling
- property labels¶
- property llabels¶
- property lmax¶
- property lmin¶
- property mlabels¶
- property mmax¶
- property mmin¶
- property ncoeffs¶
- property phis¶
- property points¶
Image-space points.
- property real¶
Transform for real signals.
If the transform is already for real signals (\(m_\mathrm{min} \geq 0\)), then this property returns self. Otherwise it returns an equivalent transform with \(m_\mathrm{min} = 0\).
- property real_dtype¶
- sampling¶
Image-space sampling
- property slabels¶
- property smax¶
- property smin¶
- property thetas¶
- class tensossht.transforms.forward.ForwardTransform(images_to_fmm, fmm_to_gmm, gmm_to_coeffs)[source]¶
- Parameters:
images_to_fmm (Callable) –
fmm_to_gmm (Callable) –
gmm_to_coeffs (GmmToCoeffs) –
- class tensossht.transforms.inverse.InverseTransform(coeffs_to_fmm, fft, slicer)[source]¶
- Parameters:
coeffs_to_fmm (Callable[[ndarray, HarmonicAxes], ndarray]) –
fft (Callable[[ndarray], ndarray]) –
slicer (Callable[[ndarray], ndarray]) –