sandbox/acastillo/output_fields/spectra/spectra_shell.h

    Shell-averaged spectra of a plane

    The transform and binning behind spectra.h. A plane sampled by spectra_sample.h is transformed with GSL and reduced to E(k) by averaging over shells of constant |k|.

    Bin 0 holds the squared mean, so \sum_k E(k) is the mean square of the plane and \sum_{k>0} E(k) its variance – an exact Parseval check. Bins beyond m/2 are only partly populated, since they exist only in the corners of the k plane; they are kept so that the sum stays exact.

    #include <gsl/gsl_fft_complex.h>
    #pragma autolink -lgsl -lgslcblas
    
    #ifndef REAL // also defined by the Dimonte initial-condition headers
      #define REAL(z,i) ((z)[2*(i)])
      #define IMAG(z,i) ((z)[2*(i)+1])
    #endif
    
    #include "spectra_sample.h"

    Forward twin of fft2D() in initial_conditions_dimonte_fft2.h. Mixed radix, so any transform length.

    static void fft2D_forward (double * data, int n0, int n1)
    {
      gsl_fft_complex_wavetable * wt = gsl_fft_complex_wavetable_alloc (n1);
      gsl_fft_complex_workspace * ws = gsl_fft_complex_workspace_alloc (n1);
      for (int i = 0; i < n0; i++)
        gsl_fft_complex_forward (&REAL(data, i*n1), 1, n1, wt, ws);
      gsl_fft_complex_wavetable_free (wt);
      gsl_fft_complex_workspace_free (ws);
    
      wt = gsl_fft_complex_wavetable_alloc (n0);
      ws = gsl_fft_complex_workspace_alloc (n0);
      double * col = malloc (2*n0*sizeof(double));
      for (int j = 0; j < n1; j++) {
        for (int i = 0; i < n0; i++) {
          REAL(col,i) = REAL(data, i*n1 + j);
          IMAG(col,i) = IMAG(data, i*n1 + j);
        }
        gsl_fft_complex_forward (col, 1, n0, wt, ws);
        for (int i = 0; i < n0; i++) {
          REAL(data, i*n1 + j) = REAL(col,i);
          IMAG(data, i*n1 + j) = IMAG(col,i);
        }
      }
      free (col);
      gsl_fft_complex_wavetable_free (wt);
      gsl_fft_complex_workspace_free (ws);
    }
    
    // Number of shells for an m1 x m2 lattice: the corner of the k plane.
    static int nshells (int m1, int m2)
    {
      return (int) (sqrt (sq (m1/2.) + sq (m2/2.)) + 0.5) + 1;
    }

    Shell-average |F|^2/(m_1m_2)^2 into unit-width bins in |k|, with k in units of 2\pi/L_0. GSL’s transform is unnormalised, hence the 1/(m_1m_2)^2.

    static void shell_average (double * data, int m1, int m2, double * E, int nk)
    {
      for (int b = 0; b < nk; b++)
        E[b] = 0.;
      double norm = 1./sq ((double) m1*m2);
      for (int i = 0; i < m1; i++) {
        double kx = i <= m1/2 ? i : i - m1;
        for (int j = 0; j < m2; j++) {
          double ky = j <= m2/2 ? j : j - m2;
          int b = (int) (sqrt (sq (kx) + sq (ky)) + 0.5);
          if (b < nk)
            E[b] += (sq (REAL(data, i*m2 + j)) + sq (IMAG(data, i*m2 + j)))*norm;
        }
      }
    }

    Cross-shell-average \mathrm{Re}[F_1 F_2^*]/(m_1m_2)^2 into the same unit-width |k| bins as shell_average(). Reduces to shell_average(data, data, ...) when the two transforms coincide, and to 0 on every bin where the two fields carry power on disjoint Fourier modes – the cross terms outside a shared (k_x,k_y) never appear in the sum.

    static void cross_shell_average (double * data1, double * data2,
                                     int m1, int m2, double * E, int nk)
    {
      for (int b = 0; b < nk; b++)
        E[b] = 0.;
      double norm = 1./sq ((double) m1*m2);
      for (int i = 0; i < m1; i++) {
        double kx = i <= m1/2 ? i : i - m1;
        for (int j = 0; j < m2; j++) {
          double ky = j <= m2/2 ? j : j - m2;
          int b = (int) (sqrt (sq (kx) + sq (ky)) + 0.5);
          if (b < nk)
            E[b] += (REAL(data1, i*m2 + j)*REAL(data2, i*m2 + j) +
                     IMAG(data1, i*m2 + j)*IMAG(data2, i*m2 + j))*norm;
        }
      }
    }

    Sample the plane at z = h and fill E with one shell-averaged spectrum per field, laid out as E[is*nk + b] for field is in bin b. The caller allocates len*nk doubles, nk from nshells(). Returns the number of lattice points no rank owned, 0 for a complete plane.

    m must be the finest grid size, or the plane is not sampled on cell centres. Only rank 0 transforms, since the sampled plane is replicated everywhere.

    int spectrum_plane (scalar * list, double * E, double h,
                        double xmin, double xmax, double ymin, double ymax, int m)
    {
      int len = list_len (list), nk = nshells (m, m);
      size_t npt = (size_t) m*m;
      double * plane = malloc (npt*len*sizeof(double));
      int holes = sample_scalar_plane (list, plane, h,
                                       xmin, xmax, ymin, ymax, m, m);
      if (pid() == 0) {
        double * data = malloc (2*npt*sizeof(double));
        for (int is = 0; is < len; is++) {
          for (size_t i = 0; i < npt; i++) {
            REAL(data,i) = plane[i*len + is];
            IMAG(data,i) = 0.;
          }
          fft2D_forward (data, m, m);
          shell_average (data, m, m, E + (size_t) is*nk, nk);
        }
        free (data);
      }
      free (plane);
      return holes/len;   // per lattice point, not per stored value
    }

    Cross-spectrum twin of spectrum_plane(): sample two fields at z = h and fill E with their shell-averaged \mathrm{Re}[\hat a \hat b^*]. Reduces to an auto-spectrum when b and a are the same field. holes is reported from a’s sample only – it depends on domain coverage, not field values, so both samples return the same count (see sample_scalar_plane()).

    int cross_spectrum_plane (scalar a, scalar b, double * E, double h,
                              double xmin, double xmax, double ymin, double ymax,
                              int m)
    {
      int nk = nshells (m, m);
      size_t npt = (size_t) m*m;
      double * pa = malloc (npt*sizeof(double));
      double * pb = malloc (npt*sizeof(double));
      int holes = sample_scalar_plane ({a}, pa, h, xmin, xmax, ymin, ymax, m, m);
      sample_scalar_plane ({b}, pb, h, xmin, xmax, ymin, ymax, m, m);
      if (pid() == 0) {
        double * da = malloc (2*npt*sizeof(double));
        double * db = malloc (2*npt*sizeof(double));
        for (size_t i = 0; i < npt; i++) {
          REAL(da,i) = pa[i], IMAG(da,i) = 0.;
          REAL(db,i) = pb[i], IMAG(db,i) = 0.;
        }
        fft2D_forward (da, m, m);
        fft2D_forward (db, m, m);
        cross_shell_average (da, db, m, m, E, nk);
        free (da);
        free (db);
      }
      free (pa);
      free (pb);
      return holes;
    }

    Foliated twin of cross_spectrum_plane(): sum each field’s anomaly over nz slabs with sample_scalar_stack_sum() (see spectra_sample.h), then cross-shell-average the two resulting planes. means_a/means_b follow that function’s means layout, one per field. holes is the sum over both fields and all slabs.

    int cross_spectrum_scalar_foliated (scalar a, scalar b,
                                        const double * means_a,
                                        const double * means_b,
                                        double * E,
                                        double hmin, double hmax, int nz,
                                        double xmin, double xmax,
                                        double ymin, double ymax, int m)
    {
      int nk = nshells (m, m);
      size_t npt = (size_t) m*m;
      double * pa = malloc (npt*sizeof(double));
      double * pb = malloc (npt*sizeof(double));
      double * z = malloc ((size_t) nz*sizeof(double));
      int holes = sample_scalar_stack_sum ({a}, pa, means_a, z, hmin, hmax, nz,
                                           xmin, xmax, ymin, ymax, m, m);
      holes += sample_scalar_stack_sum ({b}, pb, means_b, z, hmin, hmax, nz,
                                        xmin, xmax, ymin, ymax, m, m);
      if (pid() == 0) {
        double * da = malloc (2*npt*sizeof(double));
        double * db = malloc (2*npt*sizeof(double));
        for (size_t i = 0; i < npt; i++) {
          REAL(da,i) = pa[i], IMAG(da,i) = 0.;
          REAL(db,i) = pb[i], IMAG(db,i) = 0.;
        }
        fft2D_forward (da, m, m);
        fft2D_forward (db, m, m);
        cross_shell_average (da, db, m, m, E, nk);
        free (da);
        free (db);
      }
      free (pa);
      free (pb);
      free (z);
      return holes;
    }