Scattering Covariance — FoCUS / scat_cov#
Modules: foscat.scat_cov (HEALPix / spherical), foscat.scat_cov2D (2D planar), foscat.scat_cov1D (1D)
The primary entry point is the funct factory function in each module. It constructs
a FoCUS instance — the core operator that builds wavelet filters, manages per-scale
stencil tables, and evaluates scattering-covariance statistics.
Quick start#
import foscat.scat_cov as sc # HEALPix / spherical
import foscat.scat_cov2D as sc2d # 2D planar
# Create an operator for HEALPix maps (nside up to nstep_max resolution levels)
scat_op = sc.funct(KERNELSZ=5, NORIENT=4, OSTEP=1, nstep_max=4, all_type='float64')
# Evaluate statistics on a full-sky map
import numpy as np
nside = 64
x = np.random.randn(12 * nside**2)
stat = scat_op.eval(x)
print("Number of descriptors:", stat.numel)
funct — constructor (HEALPix)#
foscat.scat_cov.funct(
NORIENT = 4,
LAMBDA = 1.2,
KERNELSZ = 3,
slope = 1.0,
all_type = "float32",
nstep_max = 20,
padding = "SAME",
gpupos = 0,
mask_thres = None,
mask_norm = False,
isMPI = False,
TEMPLATE_PATH = None,
BACKEND = "torch",
use_2D = False,
use_1D = False,
return_data = False,
DODIV = False,
use_median = False,
InitWave = None,
silent = True,
mpi_size = 1,
mpi_rank = 0,
)
Parameters#
Parameter |
Type |
Default |
Description |
|---|---|---|---|
|
int |
4 |
Number of wavelet orientations. Orientations are uniformly spaced over \([0, \pi)\). Use 1 for an isotropic (orientation-averaged) operator. |
|
float |
1.2 |
Frequency parameter of the complex Morlet wavelet. Higher values shift the wavelet towards higher spatial frequencies. |
|
int |
3 |
Side length \(K\) of the local stencil (\(K \times K\) neighbours per pixel). Common values: 3, 5. Larger values increase the per-layer receptive field. |
|
float |
1.0 |
Activation slope applied to wavelet modulus (leaky-ReLU-like). Rarely needs changing. |
|
str |
|
Numerical precision: |
|
int |
20 |
Maximum number of resolution levels (HEALPix |
|
str |
|
Padding mode for 2D operators: |
|
int |
0 |
Index into the list of available CUDA devices. Wraps around the number of GPUs. |
|
float|None |
None |
If set, pixels with mask value below this threshold are excluded from statistics. |
|
bool |
False |
Normalise statistics by the fraction of unmasked pixels at each scale. |
|
bool |
False |
Enable MPI-parallel mode (requires |
|
str|None |
None |
Path for the wavelet stencil cache. Defaults to |
|
str |
|
Computation backend. Currently only |
|
bool |
False |
If True, operate on 2D grids instead of HEALPix (equivalent to using |
|
bool |
False |
If True, operate on 1D arrays (equivalent to using |
|
bool |
False |
If True, |
|
bool |
False |
Add two extra divergence-sensitive wavelet orientations (used for polarisation analysis). |
|
bool |
False |
Use median instead of mean for spatial pooling. More robust to pixel outliers. |
|
array|None |
None |
Override the initial wavelet kernels with a custom array. Shape |
|
bool |
True |
Suppress progress output during initialisation. |
|
int |
1 |
Total number of MPI ranks (set automatically when |
|
int |
0 |
Rank of this process (set automatically when |
Key attributes after construction#
scat_op.NORIENT # int — number of orientations
scat_op.KERNELSZ # int — kernel side length
scat_op.LAMBDA # float — wavelet frequency parameter
scat_op.nstep_max # int — number of resolution levels
scat_op.all_type # str — dtype string
scat_op.BACKEND # str — "torch"
scat_op.backend # BkTorch instance (low-level backend wrapper)
scat_op.ngpu # int — number of detected CUDA devices
scat_op.gpupos # int — active GPU index
eval — computing statistics#
stat = scat_op.eval(x, mask=None, norm=None)
Parameters
Parameter |
Type |
Description |
|---|---|---|
|
ndarray or Tensor, shape |
Input field(s). For HEALPix, |
|
ndarray, shape |
Binary or soft mask. Pixels with mask = 0 are excluded. |
|
scat_cov object, optional |
Normalisation statistics. If provided, each coefficient is divided by the corresponding reference value before returning. |
Returns: a scat_cov statistics object (see below).
Batched evaluation#
# batch of 8 maps at nside=64
batch = np.random.randn(8, 12 * 64**2)
stat = scat_op.eval(batch) # scat_cov object with batch dimension
scat_cov — the statistics object#
eval() returns a scat_cov instance containing the coefficient arrays:
Attribute |
Shape |
Description |
|---|---|---|
|
|
Mean wavelet power per scale |
|
|
First-order modulus mean — per scale |
|
|
Second-order scattering mean — pairs |
|
|
Cross-scale covariance — pairs |
|
|
Cross-orientation covariance — same shape as S3 |
|
|
Second-order cross-scale covariance — scale triplets, three orientations |
|
int |
Total number of real-valued coefficients after flattening |
B = batch size, Nj* = number of active scale (pairs/triplets), L = NORIENT.
Arithmetic#
scat_cov objects support element-wise arithmetic:
diff = stat_a - stat_b # difference
sq = diff ** 2 # element-wise square
loss = sq.reduce_mean_batch(sq) # scalar per batch element → mean over coefficients
Isotropic angular averaging — iso_mean / iso_ang#
For statistically isotropic fields, only the relative orientation between wavelet
pairs matters, not the absolute angle. iso_mean() reduces the orientation axes
by averaging over all global rotations, keeping only rotationally-invariant
combinations.
stat_iso = stat.iso_mean() # reduce to isotropic descriptors
stat_full = stat.iso_mean(repeat=True) # reduce then broadcast back to original shape
The reduction is different for each statistic:
S1, S2 — shape (..., L) → (...):
Simple mean over the single orientation axis:
S3, S3P — shape (..., L, L) → (..., L):
Only the angular difference \(\Delta l = l_2 - l_1 \bmod L\) is invariant. The output index is \(\Delta l\):
The \(L\) output values correspond to angular separations \(\Delta l \cdot \pi/L \in \{0, \pi/L, \ldots, (L-1)\pi/L\}\).
S4 — shape (..., L, L, L) → (..., L, L):
S4 has three orientation indices \((l_1, l_2, l_3)\), one per scale in the triplet. The two invariant quantities are both pairwise differences relative to \(l_1\):
Result shape: (..., L, L) — a \(L\times L\) matrix of relative-angle pairs.
This is implemented via the _iso_orient3 matrix in BkBase.calc_iso_orient3.
Usage with iso_ang=True in synthesis:
# Compute statistics and immediately reduce to isotropic descriptors
stat = scat_op.eval(x, norm='auto')
stat_iso = stat.iso_mean() # S1,S2 → (...), S3,S3P → (...,L), S4 → (...,L,L)
# Use iso_ang directly in synthesis:
result = scat_op.synthesis(xnorm, iso_ang=True, NUM_EPOCHS=300)
Soft angular compression — fft_ang#
iso_mean is a hard reduction: it collapses each orientation axis to a single
number (the mean), discarding all information about angular variation.
fft_ang is a softer alternative that keeps the first few Fourier harmonics
along each orientation axis, preserving the amplitude of the angular variation.
stat_fft = stat.fft_ang(nharm=1, imaginary=True)
Projection basis (nharm=1, imaginary=True) — 3 output coefficients per orientation axis:
Index k |
Basis function \(\phi_k(l)\) |
Physical meaning |
|---|---|---|
0 |
\(1\) |
DC — sum over the projected axis |
1 |
\(\cos(2\pi l/L)\) |
In-phase first harmonic |
2 |
\(\sin(2\pi l/L)\) |
Quadrature first harmonic |
Shapes after fft_ang(nharm=1, imaginary=True) (nout = 3):
Statistic |
Before |
After |
Projection axis |
|---|---|---|---|
S1, S2 |
|
|
the single orientation axis l |
S3, S3P |
|
|
l1 axis at fixed Δl = l2−l1 |
S4 |
|
|
l1 axis at fixed (Δl12, Δl13) |
For S1/S2 the projection is a straightforward 1-D Fourier basis on the orientation axis:
For S3/S3P and S4 the projection is not a tensor product. The statistics are first
reindexed by the relative-orientation differences (same regrouping as iso_mean), then
the Fourier basis is applied to the absolute orientation axis l1:
The \(k=0\) coefficient is the mean over l1 (identical to the iso_mean value at the same \(\Delta l\)). Components \(k=1,2\)
capture how the statistics vary as the image frame rotates. The amplitude
\(A_1 = \sqrt{c_1^2 + s_1^2}\) is the same for all global rotations of the field.
Why imaginary=True is essential for rotation invariance:
With imaginary=False only the cosine component is kept. A field whose dominant
orientation sits at the zero-crossing of cosine (e.g. 90° for L=4) would give
a near-zero first-harmonic coefficient despite being strongly anisotropic.
With imaginary=True both cosine and sine are kept, so the amplitude
is rotation-invariant regardless of the image orientation. This is the recommended mode whenever results must not depend on the absolute rotation of the input field.
import numpy as np
stat_fft = stat.fft_ang(nharm=1, imaginary=True)
# Rotation-invariant angular amplitude for S2:
A1_S2 = np.sqrt(stat_fft.S2[..., 1]**2 + stat_fft.S2[..., 2]**2)
Using fft_ang directly in synthesis:
The fft_ang parameter is a first-class option of synthesis, exactly like iso_ang:
# Soft angular compression — keeps DC + first harmonic amplitude (rotation-invariant)
result = scat_op.synthesis(xnorm, fft_ang=True, NUM_EPOCHS=300)
# Keep two harmonics:
result = scat_op.synthesis(xnorm, fft_ang=True, fft_nharm=2, NUM_EPOCHS=300)
fft_ang=True is applied to both the target statistics and the statistics evaluated on the current
candidate map at every optimisation step, so the loss is always comparing Fourier-compressed
statistics in a consistent space.
Advanced: custom loss with fft_ang (manual control):
from foscat.Synthesis import Loss, Synthesis
def fft_loss(u, scat_op, args):
ref_fft = args[0]
learn = scat_op.eval(u, norm='auto').fft_ang(nharm=1, imaginary=True)
return scat_op.reduce_distance(learn, ref_fft)
target_fft = scat_op.eval(xnorm, norm='auto').fft_ang(nharm=1, imaginary=True)
loss = Loss(fft_loss, scat_op, target_fft)
solver = Synthesis([loss])
result = solver.run(x0, NUM_EPOCHS=300)
Serialisation#
stat.save("my_statistics") # writes my_statistics.pkl
stat2 = foscat.scat_cov.read("my_statistics")
Converting to NumPy#
stat_np = stat.numpy() # returns a scat_cov where all arrays are numpy ndarray
Flattening#
vec = stat.flattenMask() # (B, numel) numpy array — all coefficients concatenated
ud_grade — changing HEALPix resolution#
# Downsample the operator to a coarser nside (useful for multi-resolution workflows)
low_res_map = scat_op.ud_grade_2(x, nside_out=32)
Masking#
Pass a binary mask to exclude invalid pixels (survey boundaries, missing data):
mask = np.ones(12 * nside**2)
mask[bad_pixels] = 0.0
stat = scat_op.eval(x, mask=mask)
With mask_norm=True the statistics are rescaled by the fraction of valid pixels
at each resolution level, so that partially masked statistics remain comparable to
full-sky ones.
2D operator#
foscat.scat_cov2D.funct has the same signature as the HEALPix version. The
differences are:
xhas shape(H, W)or(B, H, W)— a regular image grid.OSTEPis not supported (all orientation pairs are evaluated).Resolution levels are obtained by 2× average pooling.
import foscat.scat_cov2D as sc2d
scat_op = sc2d.funct(KERNELSZ=5, NORIENT=4, all_type='float64')
image = np.random.randn(256, 256)
stat = scat_op.eval(image)