/*
 *  $Id: stats--acfpsdf.c 29732 2026-04-03 20:31:34Z yeti-dn $
 *  Copyright (C) 2003-2025 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <string.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/simplefft.h"
#include "libgwyddion/field.h"
#include "libgwyddion/filters.h"
#include "libgwyddion/correct.h"
#include "libgwyddion/arithmetic.h"
#include "libgwyddion/level.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/linestats.h"
#include "libgwyddion/inttrans.h"

#include "libgwyddion/omp.h"
#include "libgwyddion/fftw.h"
#include "libgwyddion/internal.h"

static void
gwy_field_area_func_fft(GwyField *field,
                        GwyLine *target_line,
                        GwyFFTAreaFunc func,
                        gint col, gint row,
                        gint width, gint height,
                        GwyOrientation orientation)
{
    GwyLine *din, *dout;
    fftw_plan plan;
    gdouble *in, *out, *drow, *dcol;
    gint i, j, xres, yres, res = 0;
    gdouble avg;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;
    g_return_if_fail(GWY_IS_LINE(target_line));
    xres = field->xres;
    yres = field->yres;
    g_return_if_fail(orientation == GWY_ORIENTATION_HORIZONTAL || orientation == GWY_ORIENTATION_VERTICAL);

    if (orientation == GWY_ORIENTATION_VERTICAL) {
        res = gwy_fft_find_nice_size(2*yres);
        gwy_line_resize(target_line, height);
    }
    else {
        res = gwy_fft_find_nice_size(2*xres);
        gwy_line_resize(target_line, width);
    }
    gwy_line_clear(target_line);
    gwy_line_set_offset(target_line, 0.0);

    din = gwy_line_new(res, 1.0, FALSE);
    dout = gwy_line_new(res, 1.0, FALSE);
    in = gwy_line_get_data(din);
    out = gwy_line_get_data(dout);
    plan = gwy_fftw_plan_r2r_1d(res, in, out, FFTW_R2HC, FFTW_ESTIMATE);
    g_return_if_fail(plan);

    if (orientation == GWY_ORIENTATION_VERTICAL) {
        for (i = 0; i < width; i++) {
            dcol = field->priv->data + row*xres + (i + col);
            avg = gwy_field_area_mean(field, NULL, GWY_MASK_IGNORE, col+i, row, 1, height);
            for (j = 0; j < height; j++)
                in[j] = dcol[j*xres] - avg;
            func(plan, din, dout, target_line);
        }
        gwy_line_set_real(target_line, gwy_field_itor(field, height));
        gwy_line_multiply(target_line, 1.0/(res*width));
    }
    else {
        for (i = 0; i < height; i++) {
            drow = field->priv->data + (i + row)*xres + col;
            avg = gwy_field_area_mean(field, NULL, GWY_MASK_IGNORE, col, row+i, width, 1);
            for (j = 0; j < width; j++)
                in[j] = drow[j] - avg;
            func(plan, din, dout, target_line);
        }
        gwy_line_set_real(target_line, gwy_field_jtor(field, width));
        gwy_line_multiply(target_line, 1.0/(res*height));
    }

    fftw_destroy_plan(plan);
    g_object_unref(din);
    g_object_unref(dout);
}

/**
 * gwy_field_area_acf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @orientation: Orientation of lines (ACF is simply averaged over the other orientation).
 *
 * Calculates one-dimensional autocorrelation function of a rectangular part of a data field.
 **/
void
gwy_field_area_acf(GwyField *field,
                   GwyLine *target_line,
                   gint col, gint row,
                   gint width, gint height,
                   GwyOrientation orientation)
{
    gwy_field_area_func_fft(field, target_line, &do_fft_acf, col, row, width, height, orientation);
    /* Set proper units */
    _gwy_copy_unit(field->priv->unit_xy, &target_line->priv->unit_x);
    gwy_unit_power(gwy_line_get_unit_y(target_line), gwy_field_get_unit_z(field), 2);
}

/**
 * gwy_field_acf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @orientation: Orientation of lines (ACF is simply averaged over the other orientation).
 *
 * Calculates one-dimensional autocorrelation function of a data field.
 **/
void
gwy_field_acf(GwyField *field,
              GwyLine *target_line,
              GwyOrientation orientation)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_acf(field, target_line, 0, 0, field->xres, field->yres, orientation);
}

/**
 * gwy_field_area_hhcf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @orientation: Orientation of lines (HHCF is simply averaged over the other orientation).
 *
 * Calculates one-dimensional autocorrelation function of a rectangular part of a data field.
 **/
void
gwy_field_area_hhcf(GwyField *field,
                    GwyLine *target_line,
                    gint col, gint row,
                    gint width, gint height,
                    GwyOrientation orientation)
{
    gwy_field_area_func_fft(field, target_line, &do_fft_hhcf, col, row, width, height, orientation);

    /* Set proper units */
    _gwy_copy_unit(field->priv->unit_xy, &target_line->priv->unit_x);
    gwy_unit_power(gwy_line_get_unit_y(target_line), gwy_field_get_unit_z(field), 2);
}

/**
 * gwy_field_hhcf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @orientation: Orientation of lines (HHCF is simply averaged over the other orientation).
 *
 * Calculates one-dimensional autocorrelation function of a data field.
 **/
void
gwy_field_hhcf(GwyField *field,
               GwyLine *target_line,
               GwyOrientation orientation)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_hhcf(field, target_line, 0, 0, field->xres, field->yres, orientation);
}

/**
 * gwy_field_area_psdf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @orientation: Orientation of lines (PSDF is simply averaged over the other orientation).
 * @windowing: Windowing type to use.
 *
 * Calculates one-dimensional power spectral density function of a rectangular part of a data field.
 **/
void
gwy_field_area_psdf(GwyField *field,
                    GwyLine *target_line,
                    gint col, gint row,
                    gint width, gint height,
                    GwyOrientation orientation,
                    GwyWindowingType windowing)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;

    GwyField *re_field, *im_field;
    gdouble *re, *im, *target;
    gint i, j, xres, yres, size;

    g_return_if_fail(GWY_IS_LINE(target_line));
    xres = field->xres;
    yres = field->yres;
    size = (orientation == GWY_ORIENTATION_HORIZONTAL) ? width : height;
    g_return_if_fail(size >= 4);
    g_return_if_fail(orientation == GWY_ORIENTATION_HORIZONTAL || orientation == GWY_ORIENTATION_VERTICAL);

    gwy_line_resize(target_line, size/2);
    gwy_line_clear(target_line);
    gwy_line_set_offset(target_line, 0.0);

    re_field = gwy_field_new(width, height, 1.0, 1.0, FALSE);
    im_field = gwy_field_new(width, height, 1.0, 1.0, FALSE);
    target = target_line->priv->data;
    if (orientation == GWY_ORIENTATION_VERTICAL) {
        gwy_field_area_fft_1d(field, NULL, re_field, im_field, col, row, width, height,
                              orientation, windowing, GWY_TRANSFORM_DIRECTION_FORWARD, TRUE, 2);
        re = re_field->priv->data;
        im = im_field->priv->data;
        for (i = 0; i < width; i++) {
            for (j = 0; j < size/2; j++)
                target[j] += re[j*width + i]*re[j*width + i] + im[j*width + i]*im[j*width + i];
        }
        gwy_line_multiply(target_line, field->yreal/yres/(2*G_PI*width));
        gwy_line_set_real(target_line, G_PI*yres/field->yreal);
    }
    else {
        gwy_field_area_fft_1d(field, NULL, re_field, im_field, col, row, width, height,
                              orientation, windowing, GWY_TRANSFORM_DIRECTION_FORWARD, TRUE, 2);
        re = re_field->priv->data;
        im = im_field->priv->data;
        for (i = 0; i < height; i++) {
            for (j = 0; j < size/2; j++)
                target[j] += re[i*width + j]*re[i*width + j] + im[i*width + j]*im[i*width + j];
        }
        gwy_line_multiply(target_line, field->xreal/xres/(2*G_PI*height));
        gwy_line_set_real(target_line, G_PI*xres/field->xreal);
    }

    gwy_line_set_offset(target_line, target_line->real/target_line->res);
    gwy_line_crop(target_line, 1, target_line->res-1);

    g_object_unref(re_field);
    g_object_unref(im_field);

    /* Set proper units */
    gwy_unit_power(gwy_line_get_unit_x(target_line), gwy_field_get_unit_xy(field), -1);
    gwy_unit_power_multiply(gwy_line_get_unit_y(target_line),
                            gwy_field_get_unit_z(field), 2,
                            gwy_field_get_unit_xy(field), 1);
}

static void
gwy_field_area_ri_psdf_common(GwyField *field,
                              GwyLine *target_line,
                              gint col, gint row,
                              gint width, gint height,
                              GwyWindowingType windowing,
                              gint *nstats)
{
    gint xres = field->xres, yres = field->yres;
    gdouble xreal = field->xreal, yreal = field->yreal;
    gdouble *re, *im;
    gint i, j, k;
    gdouble r;

    GwyField *re_field = gwy_field_new(width, height, width*xreal/xres, height*yreal/yres, FALSE);
    GwyField *im_field = gwy_field_new_alike(re_field, FALSE);
    gwy_field_area_fft_2d(field, NULL, re_field, im_field, col, row, width, height,
                          windowing, GWY_TRANSFORM_DIRECTION_FORWARD, TRUE, 2);
    re = re_field->priv->data;
    im = im_field->priv->data;
    for (i = 0; i < height; i++) {
        for (j = 0; j < width; j++) {
            k = i*width + j;
            re[k] = re[k]*re[k] + im[k]*im[k];
        }
    }
    g_object_unref(im_field);

    gwy_field_fft_postprocess(re_field, TRUE);
    r = 0.5*MAX(re_field->xreal, re_field->yreal);
    GwyLine *rline = gwy_field_angular_average(re_field, NULL, GWY_MASK_IGNORE, 0.0, 0.0, r, *nstats ? *nstats+1 : -1);
    g_object_unref(re_field);
    gwy_line_assign(target_line, rline);
    g_object_unref(rline);
    /* Get rid of the zero first element which is bad for logscale. */
    *nstats = target_line->res-1;
    gwy_line_crop(target_line, 1, *nstats);
    target_line->off += target_line->real/(*nstats);

    /* Postprocess does not use angular coordinates, fix that. */
    target_line->real *= 2.0*G_PI;
    target_line->off *= 2.0*G_PI;

    /* Set proper value units */
    gwy_unit_power(gwy_line_get_unit_x(target_line), gwy_field_get_unit_xy(field), -1);
    gwy_unit_power_multiply(gwy_line_get_unit_y(target_line),
                            gwy_field_get_unit_z(field), 2,
                            gwy_field_get_unit_xy(field), 1);
}

/**
 * gwy_field_psdf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @orientation: Orientation of lines (PSDF is simply averaged over the other orientation).
 * @windowing: Windowing type to use.
 *
 * Calculates one-dimensional power spectral density function of a data field.
 **/
void
gwy_field_psdf(GwyField *field,
               GwyLine *target_line,
               GwyOrientation orientation,
               GwyWindowingType windowing)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_psdf(field, target_line, 0, 0, field->xres, field->yres, orientation, windowing);
}

/**
 * gwy_field_area_rpsdf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @windowing: Windowing type to use.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, data field width (height) is
 *          used.
 *
 * Calculates radial power spectral density function of a rectangular part of a data field.
 *
 * The function calculates density in the radial wavevector. See gwy_field_area_ipsdf() for angular averaging of
 * the spectral density.
 **/
void
gwy_field_area_rpsdf(GwyField *field,
                     GwyLine *target_line,
                     gint col, gint row,
                     gint width, gint height,
                     GwyWindowingType windowing,
                     gint nstats)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;
    g_return_if_fail(GWY_IS_LINE(target_line));
    g_return_if_fail(width >= 4 && height >= 4);
    gwy_field_area_ri_psdf_common(field, target_line, col, row, width, height,
                                  windowing, &nstats);

    gdouble r = field->xreal*field->yreal/(2.0*G_PI*width*height) * target_line->real/nstats;
    for (gint k = 0; k < nstats; k++)
        target_line->priv->data[k] *= r*(k + 1);
}

/**
 * gwy_field_rpsdf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @windowing: Windowing type to use.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, data field width (height) is
 *          used.
 *
 * Calculates radial power spectral density function of a data field.
 *
 * The function calculates density in the radial wavevector. See gwy_field_ipsdf() for angular averaging of the
 * spectral density.
 **/
void
gwy_field_rpsdf(GwyField *field,
                GwyLine *target_line,
                GwyWindowingType windowing,
                gint nstats)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_rpsdf(field, target_line, 0, 0, field->xres, field->yres,
                         windowing, nstats);
}

/**
 * gwy_field_area_ipsdf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @windowing: Windowing type to use.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, data field width (height) is
 *          used.
 *
 * Calculates angularly averaged power spectral density function of a rectangular part of a data field.
 *
 * The function may seem like more intutitive analog of 1D power spectral density if the data are isotropic. However,
 * it is merely an angular average. It is not, in fact, actually any kind of spectral density. It also has the same
 * units as the 2D PSDF. See gwy_field_area_rpsdf() for density in the radial wavevector.
 **/
void
gwy_field_area_ipsdf(GwyField *field,
                     GwyLine *target_line,
                     gint col, gint row,
                     gint width, gint height,
                     GwyWindowingType windowing,
                     gint nstats)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;
    g_return_if_fail(GWY_IS_LINE(target_line));
    g_return_if_fail(width >= 4 && height >= 4);
    gwy_field_area_ri_psdf_common(field, target_line, col, row, width, height, windowing, &nstats);
    gwy_line_multiply(target_line, field->xreal*field->yreal/(4.0*G_PI*G_PI*width*height));

    /* The units must be as for 2D PSDF. */
    gwy_unit_multiply(gwy_line_get_unit_y(target_line), gwy_line_get_unit_y(target_line), gwy_field_get_unit_xy(field));
}

/**
 * gwy_field_ipsdf:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @windowing: Windowing type to use.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, data field width (height) is
 *          used.
 *
 * Calculates angularly averaged power spectral density function of a data field.
 *
 * The function may seem like more intutitive analog of 1D power spectral density if the data are isotropic. However,
 * it is merely an angular average. It is not, in fact, actually any kind of spectral density. It also has the same
 * units as the 2D PSDF. See gwy_field_rpsdf() for density in the radial wavevector.
 **/
void
gwy_field_ipsdf(GwyField *field,
                GwyLine *target_line,
                GwyWindowingType windowing,
                gint nstats)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_rpsdf(field, target_line, 0, 0, field->xres, field->yres, windowing, nstats);
}

/**
 * gwy_field_area_racf:
 * @field: A data field.
 * @target_line: A data line to store the autocorrelation function to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @nstats: The number of samples to take on the autocorrelation function.  If nonpositive, a suitable resolution is
 *          chosen automatically.
 *
 * Calculates radially averaged autocorrelation function of a rectangular part of a data field.
 **/
void
gwy_field_area_racf(GwyField *field,
                    GwyLine *target_line,
                    gint col, gint row,
                    gint width, gint height,
                    gint nstats)
{
    GwyField *acf_field;
    gint size;
    gdouble r;

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE))
        return;
    g_return_if_fail(GWY_IS_LINE(target_line));
    g_return_if_fail(width >= 4 && height >= 4);

    size = MIN(width, height)/2;
    acf_field = gwy_field_new(2*size - 1, 2*size - 1, 1.0, 1.0, FALSE);
    gwy_field_area_acf_2d(field, NULL, GWY_MASK_IGNORE, acf_field, col, row, width, height, size, size, NULL);
    r = 0.5*MAX(acf_field->xreal, acf_field->yreal);
    GwyLine *rline = gwy_field_angular_average(acf_field, NULL, GWY_MASK_IGNORE, 0.0, 0.0, r, nstats);
    g_object_unref(acf_field);
    gwy_line_assign(target_line, rline);
    g_object_unref(rline);
}

/**
 * gwy_field_racf:
 * @field: A data field.
 * @target_line: A data line to store the autocorrelation function to.  It will be resampled to requested width.
 * @nstats: The number of samples to take on the autocorrelation function.  If nonpositive, a suitable resolution is
 *          chosen automatically.
 *
 * Calculates radially averaged autocorrelation function of a data field.
 **/
void
gwy_field_racf(GwyField *field,
               GwyLine *target_line,
               gint nstats)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_racf(field, target_line, 0, 0, field->xres, field->yres, nstats);
}

static void
fix_levelling_degree(gint *level)
{
    if (*level > 2) {
        g_warning("Levelling degree %u is not supported, changing to 2.", *level);
        *level = 2;
    }
}

static void
execute_2d_acf(GwyField *extfield,
               fftw_plan plan, fftw_complex *cbuf, guint cstride,
               guint width, guint height)
{
    guint xsize = extfield->xres, ysize = extfield->yres;
    gdouble *extdata = extfield->priv->data;

    gwy_field_area_clear(extfield, width, 0, xsize - width, height);
    gwy_field_area_clear(extfield, 0, height, xsize, ysize - height);
    gwy_fftw_execute(plan);

    fftw_complex *c = cbuf;
    for (guint i = 0; i < ysize; i++) {
        gdouble *row1 = extdata + i*xsize;
        gdouble *row2 = extdata + ((ysize - i) % ysize)*xsize + xsize-1;
        *(row1++) = gwy_cnorm(*c);
        c++;
        for (guint j = 1; j < cstride; j++) {
            gdouble re = gwy_cnorm(*c);
            *(row1++) = re;
            *(row2--) = re;
            c++;
        }
    }
    gwy_fftw_execute(plan);
}

static void
extract_2d_acf_real(GwyField *field,
                    fftw_complex *cbuf, guint cstride, guint ysize)
{
    guint txres = field->xres, tyres = field->yres;
    guint xrange = txres/2 + 1;
    guint yrange = tyres/2 + 1;

    for (guint i = 0; i < tyres; i++) {
        fftw_complex *c = cbuf + ((i + ysize - (yrange-1)) % ysize)*cstride;
        gdouble *row1 = field->priv->data + i*txres;
        for (guint j = xrange-1; j < txres; j++) {
            row1[j] = creal(*c);
            c++;
        }

        row1 = field->priv->data + (tyres-1 - i)*txres;
        gdouble *row2 = field->priv->data + i*txres + txres-1;
        for (guint j = 0; j < xrange-1; j++)
            *(row1++) = *(row2--);
    }
}

/**
 * gwy_field_area_acf_2d:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @target: A data field to store the result to.  It will be resampled to (2@xrange-1)×(2@yrange-1).
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @xrange: Horizontal correlation range.  Non-positive value means the default range of half of @field width
 *          will be used.
 * @yrange: Vertical correlation range.  Non-positive value means the default range of half of @field height will
 *          be used.
 * @weights: (nullable):
 *           Field to store the denominators to (or %NULL).  It will be resized like @target_field.  The denominators
 *           are integers equal to the number of terms that contributed to each value.  They are suitable as fitting
 *           weights if the ACF is fitted.
 *
 * Calculates two-dimensional autocorrelation function of a data field area.
 *
 * The resulting data field has the correlation corresponding to (0,0) in the centre.
 *
 * The maximum possible values of @xrange and @yrange are @field width and height, respectively.  However, as the
 * values for longer distances are calculated from smaller number of data points they become increasingly bogus,
 * therefore the default range is half of the size.
 **/
void
gwy_field_area_acf_2d(GwyField *field,
                      GwyNield *mask,
                      GwyMaskingType masking,
                      GwyField *target,
                      gint col, gint row,
                      gint width, gint height,
                      gint xrange, gint yrange,
                      GwyField *weights)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;
    g_return_if_fail(GWY_IS_FIELD(target));
    g_return_if_fail(!weights || GWY_IS_FIELD(weights));

    gint xres = field->xres, yres = field->yres;
    if (xrange <= 0)
        xrange = width/2;
    if (yrange <= 0)
        yrange = height/2;
    g_return_if_fail(xrange <= width && yrange <= height);

    gdouble xreal = field->xreal, yreal = field->yreal;
    gint xsize = gwy_fft_find_nice_size(width + xrange);
    gint ysize = gwy_fft_find_nice_size(height + yrange);
    gint cstride = xsize/2 + 1;

    gint txres = 2*xrange - 1;
    gint tyres = 2*yrange - 1;
    gwy_field_resize(target, txres, tyres);

    if (weights) {
        gwy_field_resize(weights, txres, tyres);
        g_object_ref(weights);
    }
    else
        weights = gwy_field_new_alike(target, FALSE);

    fftw_complex *cbuf = fftw_alloc_complex(cstride*ysize);
    GwyField *extfield = gwy_field_new(xsize, ysize, 1.0, 1.0, FALSE);
    fftw_plan plan = gwy_fftw_plan_dft_r2c_2d(ysize, xsize, extfield->priv->data, cbuf,
                                              FFTW_DESTROY_INPUT | FFTW_ESTIMATE);

    if (mask) {
        /* Calculate unnormalised ACF of the mask, i.e. the denominators. */
        for (gint i = 0; i < height; i++) {
            const gint *mrow = mask->priv->data + (i + row)*xres + col;
            gdouble *trow = extfield->priv->data + i*xsize;
            for (gint j = 0; j < width; j++)
                trow[j] = nielded_included(mrow + j, masking);
        }
        execute_2d_acf(extfield, plan, cbuf, cstride, width, height);
        extract_2d_acf_real(weights, cbuf, cstride, ysize);
        gwy_field_multiply(weights, 1.0/(xsize*ysize));

        /* Calculate unnormalised ACF of the premultiplied image, i.e. the numerators. */
        for (gint i = 0; i < height; i++) {
            const gint *mrow = mask->priv->data + (i + row)*xres + col;
            const gdouble *drow = field->priv->data + (i + row)*xres + col;
            gdouble *trow = extfield->priv->data + i*xsize;
            for (gint j = 0; j < width; j++)
                trow[j] = nielded_included(mrow + j, masking)*drow[j];
        }
    }
    else {
        gdouble *qrow = weights->priv->data;
        for (gint j = 0; j < txres; j++)
            qrow[j] = width - ABS(j - (xrange-1));
        for (gint i = 1; i < tyres; i++) {
            gint qi = height - ABS(i - (yrange-1));
            gdouble *trow = weights->priv->data + i*txres;
            for (gint j = 0; j < txres; j++)
                trow[j] = qrow[j] * qi;
        }
        for (gint j = 0; j < txres; j++)
            qrow[j] *= height - (yrange - 1);

        gwy_field_area_copy(field, extfield, col, row, width, height, 0, 0);
    }
    execute_2d_acf(extfield, plan, cbuf, cstride, width, height);
    extract_2d_acf_real(target, cbuf, cstride, ysize);
    gwy_field_multiply(target, 1.0/(xsize*ysize));

    fftw_destroy_plan(plan);
    fftw_free(cbuf);
    g_object_unref(extfield);

    if (mask) {
        GwyNield *wmask = gwy_nield_new(txres, tyres);
        gdouble thresh = 1.000001*GWY_ROUND(log(width*height));
        for (gint i = 0; i < tyres; i++) {
            const gdouble *qrow = weights->priv->data + i*txres;
            gdouble *trow = target->priv->data + i*txres;
            gint *mrow = wmask->priv->data + i*txres;
            for (gint j = 0; j < txres; j++) {
                if (qrow[j] > thresh) {
                    mrow[j] = 0;
                    trow[j] /= qrow[j];
                }
                else {
                    mrow[j] = 1;
                    trow[j] = 0.0;
                }
            }
        }
        gwy_field_laplace_solve(target, wmask, GWY_LAPLACE_MASKED, 1.0);
        g_object_unref(wmask);
    }
    else {
        gwy_field_divide_fields(target, target, weights);
    }

    target->xreal = xreal*txres/xres;
    target->yreal = yreal*tyres/yres;
    target->xoff = -0.5*target->xreal;
    target->yoff = -0.5*target->yreal;

    _gwy_copy_unit(field->priv->unit_xy, &target->priv->unit_xy);
    gwy_unit_power(gwy_field_get_unit_z(target), gwy_field_get_unit_z(field), 2);

    weights->xreal = xreal*txres/xres;
    weights->yreal = yreal*tyres/yres;
    weights->xoff = -0.5*weights->xreal;
    weights->yoff = -0.5*weights->yreal;

    _gwy_copy_unit(field->priv->unit_xy, &weights->priv->unit_xy);
    _gwy_copy_unit(NULL, &weights->priv->unit_z);

    gwy_field_invalidate(target);
    gwy_field_invalidate(weights);
    g_object_unref(weights);
}

/**
 * gwy_field_acf_2d:
 * @field: A data field.
 * @target_field: A data field to store the result to.
 *
 * Calculates two-dimensional autocorrelation function of a data field.
 *
 * See gwy_field_area_acf_2d() for details.  Parameters missing (not adjustable) in this function are set to their
 * default values.
 **/
void
gwy_field_acf_2d(GwyField *field,
                 GwyField *target_field)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_acf_2d(field, NULL, GWY_MASK_IGNORE, target_field,
                          0, 0, field->xres, field->yres, 0, 0, NULL);
}

/* Assumes @plan acts on buf->priv->data. */
static void
execute_2d_cacf(GwyField *buf, GwyField *target,
                fftw_plan plan, fftw_complex *cbuf, guint cstride)
{
    guint xres = buf->xres, yres = buf->yres;
    fftw_complex *c = cbuf;
    gdouble *data = buf->priv->data;

    gwy_fftw_execute(plan);
    for (guint i = 0; i < yres; i++) {
        gdouble *row1 = data + i*xres;
        gdouble *row2 = data + ((yres - i) % yres)*xres + xres-1;
        *(row1++) = gwy_cnorm(*c);
        c++;
        for (guint j = 1; j < cstride; j++) {
            gdouble re = gwy_cnorm(*c);
            *(row1++) = re;
            *(row2--) = re;
            c++;
        }
    }

    gwy_fftw_execute(plan);
    c = cbuf;
    data = target->priv->data;
    for (guint i = 0; i < yres; i++) {
        gdouble *row1 = data + i*xres;
        gdouble *row2 = data + ((yres - i) % yres)*xres + xres-1;
        *(row1++) = creal(*c);
        c++;
        for (guint j = 1; j < cstride; j++) {
            gdouble re = creal(*c);
            *(row1++) = re;
            *(row2--) = re;
            c++;
        }
    }
}

/* Extract real parts.  Callers might not like negative PSDF much but it is the unbiased estimate. */
static void
extract_2d_fft_real(GwyField *target,
                    fftw_plan plan, fftw_complex *cbuf, guint cstride)
{
    guint xres = target->xres, yres = target->yres;
    fftw_complex *c = cbuf;
    gdouble *data = target->priv->data;

    gwy_fftw_execute(plan);
    for (guint i = 0; i < yres; i++) {
        gdouble *row1 = data + i*xres;
        gdouble *row2 = data + ((yres - i) % yres)*xres + xres-1;
        *(row1++) = creal(*c);
        c++;
        for (guint j = 1; j < cstride; j++) {
            gdouble re = creal(*c);
            *(row1++) = re;
            *(row2--) = re;
            c++;
        }
    }
}

/* Ensure we produce non-negative output even in the presence of rounding errors.  Callers might not like negative
 * PSDF much (even though it is the unbiased estimate). */
static void
extract_2d_fft_cnorm(GwyField *target,
                     fftw_plan plan, fftw_complex *cbuf, guint cstride)
{
    guint xres = target->xres, yres = target->yres;
    fftw_complex *c = cbuf;

    gwy_fftw_execute(plan);
    gdouble *data = target->priv->data;
    for (guint i = 0; i < yres; i++) {
        gdouble *row1 = data + i*xres;
        gdouble *row2 = data + ((yres - i) % yres)*xres + xres-1;
        *(row1++) = gwy_cnorm(*c);
        c++;
        for (guint j = 1; j < cstride; j++) {
            gdouble re = gwy_cnorm(*c);
            *(row1++) = re;
            *(row2--) = re;
            c++;
        }
    }
}

/**
 * gwy_field_area_psdf_2d:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @windowing: Windowing type to use.
 * @level: The first polynomial degree to keep in the area; lower degrees than @level are subtracted.
 * @target: A data field to store the result to.  It will be resampled to @width×@height.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Calculates two-dimensional power spectral density function of a data field area.
 *
 * The resulting data field has the spectral density corresponding zero frequency (0,0) in the centre.
 *
 * Only @level values 0 (no levelling) and 1 (subtract the mean value) and 2 (subtract the mean plane) are currently
 * available. For SPM data, you usually wish to pass 1.
 *
 * The reduction of the total energy by windowing is compensated by multiplying the PSDF to make its sum of squares
 * equal to the input data sum of squares.
 *
 * Do not assume the PSDF values are all positive, when masking is in effect. The PSDF should still have the correct
 * integral, but it will be contaminated with noise, both positive and negative.
 **/
void
gwy_field_area_psdf_2d(GwyField *field,
                       GwyNield *mask,
                       GwyMaskingType masking,
                       GwyField *target,
                       gint col, gint row,
                       gint width, gint height,
                       GwyWindowingType windowing,
                       gint level)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;
    g_return_if_fail(GWY_IS_FIELD(target));

    fix_levelling_degree(&level);

    /* We cannot pad to a nice-for-FFT size because we want to calculate cyclic ACF. */
    gsize n = (gsize)width * (gsize)height;
    guint cstride = width/2 + 1;
    gwy_field_resize(target, width, height);

    fftw_complex *cbuf = fftw_alloc_complex(cstride*height);

    if (mask) {
        GwyField *buf = gwy_field_new_alike(target, FALSE);
        gdouble *b = buf->priv->data;
        fftw_plan plan = gwy_fftw_plan_dft_r2c_2d(height, width, b, cbuf, FFTW_DESTROY_INPUT | FFTW_ESTIMATE);

        /* Level and window the area. */
        gwy_field_area_copy(field, target, col, row, width, height, 0, 0);
        GwyNield *target_mask = gwy_nield_new(width, height);
        gwy_nield_area_copy(mask, target_mask, col, row, width, height, 0, 0);

        if (level > 1) {
            gdouble avg, bx, by;
            gwy_field_area_fit_plane(target, target_mask, masking, 0, 0, width, height, &avg, &bx, &by);
            gwy_field_plane_level(target, avg, bx, by);
        }
        else if (level)
            gwy_field_add(target, -gwy_field_area_mean(target, target_mask, masking, 0, 0, width, height));

        gdouble rms = gwy_field_area_rms(target, target_mask, masking, 0, 0, width, height);
        gwy_field_fft_window(target, windowing);

        gdouble newrms = gwy_field_area_rms(target, target_mask, masking, 0, 0, width, height);
        if (newrms > 0.0)
            gwy_field_multiply(target, rms/newrms);

        /* Calculate unnormalised CACF of the mask, i.e. the denominators. */
        gwy_field_fill_mask(buf, target_mask, 0.0, 1.0);
        gwy_field_multiply_fields(target, buf, target);

        GwyField *weights = gwy_field_new_alike(target, FALSE);
        execute_2d_cacf(buf, weights, plan, cbuf, cstride);

        /* Calculate unnormalised CACF of the premultiplied image, i.e. the numerators. */
        gwy_field_copy_data(target, buf);
        execute_2d_cacf(buf, target, plan, cbuf, cstride);

        /* Divide, with interpolation of missing values. */
        gdouble thresh = 1.000001*GWY_ROUND(log(n));
        const gdouble *w = weights->priv->data;
        gdouble *t = target->priv->data;
        gint *m = target_mask->priv->data;
        for (gsize i = 0; i < n; i++) {
            if (w[i] > thresh) {
                m[i] = 0;
                b[i] = t[i]/w[i];
            }
            else {
                m[i] = 1;
                b[i] = 0.0;
            }
        }
        gwy_field_laplace_solve(buf, target_mask, GWY_LAPLACE_MASKED, 1.0);
        g_object_unref(target_mask);

        extract_2d_fft_real(target, plan, cbuf, cstride);
        fftw_destroy_plan(plan);

        g_object_unref(weights);
        g_object_unref(buf);
    }
    else {
        gdouble *t = target->priv->data;
        fftw_plan plan = gwy_fftw_plan_dft_r2c_2d(height, width, t, cbuf, FFTW_DESTROY_INPUT | FFTW_ESTIMATE);

        /* Level and window the area. */
        gwy_field_area_copy(field, target, col, row, width, height, 0, 0);
        if (level > 1) {
            gdouble avg, bx, by;
            gwy_field_fit_plane(target, &avg, &bx, &by);
            gwy_field_plane_level(target, avg, bx, by);
        }
        else if (level)
            gwy_field_add(target, -gwy_field_mean(target));

        gdouble rms = gwy_field_rms(target);
        gwy_field_fft_window(target, windowing);
        gdouble newrms = gwy_field_rms(target);
        if (newrms > 0.0)
            gwy_field_multiply(target, rms/newrms);

        /* Do the FFT and gather squared Fourier coeffs. */
        extract_2d_fft_cnorm(target, plan, cbuf, cstride);
        gwy_field_multiply(target, 1.0/n);
        fftw_destroy_plan(plan);
    }
    gwy_field_fft_2d_center(target);

    fftw_free(cbuf);

    gwy_unit_power(gwy_field_get_unit_xy(target), gwy_field_get_unit_xy(field), -1);
    gwy_unit_power_multiply(gwy_field_get_unit_z(target),
                            gwy_field_get_unit_xy(field), 2,
                            gwy_field_get_unit_z(field), 2);

    gwy_field_set_xreal(target, 2.0*G_PI/gwy_field_get_dx(field));
    gwy_field_set_yreal(target, 2.0*G_PI/gwy_field_get_dy(field));

    gdouble rx = (width + 1 - width % 2)/2.0;
    gwy_field_set_xoffset(target, -gwy_field_jtor(target, rx));

    gdouble ry = (height + 1 - height % 2)/2.0;
    gwy_field_set_yoffset(target, -gwy_field_itor(target, ry));

    gwy_field_multiply(target, 1.0/(target->xreal*target->yreal));
    gwy_field_invalidate(target);
}

/**
 * gwy_field_psdf_2d:
 * @field: A data field.
 * @windowing: Windowing type to use.
 * @level: The first polynomial degree to keep in the area; lower degrees than @level are subtracted.  Note only
 *         values 0, 1, and 2 are available at present. For SPM data, you usually wish to pass 1.
 * @target_field: A data field to store the result to.  It will be resampled to the same size as @field.
 *
 * Calculates two-dimensional power spectral density function of a data field.
 *
 * See gwy_field_area_psdf_2d_mask() for details and discussion.
 **/
void
gwy_field_psdf_2d(GwyField *field,
                  GwyField *target,
                  GwyWindowingType windowing,
                  gint level)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_psdf_2d(field, NULL, GWY_MASK_IGNORE, target, 0, 0, field->xres, field->yres,
                           windowing, level);
}

/**
 * gwy_field_psdf_to_angular_spectrum:
 * @psdf: A data field containing 2D spectral density, humanized (i.e. with zero frequency in the centre).
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, a suitable number is chosen
 *          automatically.
 *
 * Transforms 2D power spectral density to an angular spectral.
 *
 * Returns: (transfer full): A new one-dimensional data line with the angular spectral.
 **/
GwyLine*
gwy_field_psdf_to_angular_spectrum(GwyField *psdf,
                                   gint nstats)
{
    gint xres, yres, nn, k;
    GwyLine *angspec;
    const gdouble *d;
    gdouble xreal, yreal, asum, wsum;
    gdouble *a;

    g_return_val_if_fail(GWY_IS_FIELD(psdf), NULL);

    xres = psdf->xres;
    yres = psdf->yres;
    xreal = psdf->xreal;
    yreal = psdf->yreal;
    d = psdf->priv->data;
    nn = xres*yres;

    if (nstats < 1) {
        nstats = floor(5.49*cbrt(nn) + 0.5);
        nstats = MAX(nstats, 2);
        nstats += (nstats & 1);
    }

    angspec = gwy_line_new(nstats, 2.0*G_PI, TRUE);
    gwy_line_set_offset(angspec, -G_PI/nstats);
    a = angspec->priv->data;
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(xres,yres,xreal,yreal,nstats,d,a) private(k)
#endif
    for (k = 0; k < nstats; k++) {
        const gdouble step = 0.14589803375031551;
        gdouble alpha0 = k*2.0*G_PI/nstats, s = 0.0;
        gint kk = 0, mm = 0;

        for (kk = -5; kk <= 5; kk++) {
            gdouble alpha = alpha0 + kk/10.0*2.0*G_PI/nstats;
            /* Now we have to transform the real-space angle to an image in the reciprocal space.  For non-square
             * pixels it changes. */
            gdouble Kxsa = xreal/xres * sin(alpha);
            gdouble Kyca = yreal/yres * cos(alpha);
            gdouble h = sqrt(Kxsa*Kxsa + Kyca*Kyca);
            gdouble cb = Kyca/h, sb = Kxsa/h;
            gint m = 0;

            while (TRUE) {
                /* Zero frequency is always at res/2 in humanized data. */
                gdouble x = xres/2 + 0.5 + m*step*cb;
                gdouble y = yres/2 + 0.5 - m*step*sb;
                gint i = floor(y), j = floor(x);
                if (i < 0 || i > yres-1 || j < 0 || j > xres-1)
                    break;

                s += d[i*xres + j];
                m++;
            }
            mm += m;
        }
        if (mm)
            a[k] = s/mm;
    }

    /* The result now has some weird normalisation depending on how we hit and missed the pixels.  Preserve the
     * integral which must be sigma^2 (but we only make half of the curve). */
    wsum = gwy_field_sum(psdf) * xreal/xres * yreal/yres;
    asum = gwy_line_sum(angspec) * 2.0*G_PI/nstats;
    if (asum > 0.0)
        gwy_line_multiply(angspec, wsum/asum);

    gwy_unit_power_multiply(gwy_line_get_unit_y(angspec),
                            gwy_field_get_unit_z(psdf), 1,
                            gwy_field_get_unit_xy(psdf), 2);

    return angspec;
}

/* Does not really belong here, but is is used only by functions from this source file, so... */
/**
 * gwy_field_angular_average:
 * @field: A data field.
 * @mask: (nullable): Mask of pixels to include from/exclude in the averaging, or %NULL for full @field.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @x: X-coordinate of the averaging disc origin, in real coordinates including offsets.
 * @y: Y-coordinate of the averaging disc origin, in real coordinates including offsets.
 * @r: Radius, in real coordinates.  It determines the real length of the resulting line.
 * @nstats: The number of samples the resulting line should have.  A non-positive value means the sampling will be
 *          determined automatically.
 *
 * Performs angular averaging of a part of a data field.
 *
 * The result of such averaging is an radial profile, starting from the disc centre.
 *
 * The function does not guarantee that @target_line will have exactly @nstats samples upon return.  A smaller number
 * of samples than requested may be calculated for instance if either central or outer part of the disc is excluded by
 * masking.
 *
 * Returns: A new data line to with the distribution.
 **/
GwyLine*
gwy_field_angular_average(GwyField *field,
                          GwyNield *mask,
                          GwyMaskingType masking,
                          gdouble x,
                          gdouble y,
                          gdouble r,
                          gint nstats)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    g_return_val_if_fail(r >= 0.0, NULL);
    if (!_gwy_field_check_mask(field, &mask, &masking))
        return NULL;

    gint xres = field->xres, yres = field->yres;
    gdouble xreal = field->xreal, yreal = field->yreal;
    gdouble dx = xreal/xres, dy = yreal/yres;
    gdouble xoff = field->xoff, yoff = field->yoff;
    g_return_val_if_fail(x >= xoff && x <= xoff + xreal, NULL);
    g_return_val_if_fail(y >= yoff && y <= yoff + yreal, NULL);

    /* Just for integer overflow; we limit i and j ranges explicitly later. */
    r = MIN(r, hypot(xreal, yreal));
    x -= xoff;
    y -= yoff;

    /* Prefer sampling close to the shorter step. And if dx=dy use exactly the same step; not slightly different which
     * is usually just annoying. */
    gdouble h;
    if (nstats < 1) {
        h = 2.0*dx*dy/(dx + dy);
        nstats = GWY_ROUND(r/h);
        nstats = MAX(nstats, 1);
        r = nstats*h;
    }
    else
        h = r/nstats;

    const gdouble *d = field->priv->data;
    const gint *m = mask ? mask->priv->data : NULL;

    GwyLine *line = gwy_line_new(nstats, h*nstats, TRUE);
    gwy_field_copy_units_to_line(field, line);
    line->off = 0.0;
    gdouble *target = line->priv->data;
    /* Just return something for single-point lines. */
    if (nstats < 2 || r == 0.0) {
        /* NB: gwy_field_get_dval_real() does not use offsets. */
        target[0] = gwy_field_get_dval_real(field, x, y, GWY_INTERPOLATION_ROUND);
        return line;
    }

    gint ifrom = (gint)floor(gwy_field_rtoi(field, y - r));
    ifrom = MAX(ifrom, 0);
    gint ito = (gint)ceil(gwy_field_rtoi(field, y + r));
    ito = MIN(ito, yres-1);

    gint jfrom = (gint)floor(gwy_field_rtoj(field, x - r));
    jfrom = MAX(jfrom, 0);
    gint jto = (gint)ceil(gwy_field_rtoj(field, x + r));
    jto = MIN(jto, xres-1);

    gdouble *weight = g_new0(gdouble, nstats);
#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(d,m,target,weight,ifrom,jfrom,ito,jto,masking,xres,yres,nstats,h,x,y,dx,dy)
#endif
    {
        gint tifrom = gwy_omp_chunk_start(ito+1 - ifrom) + ifrom;
        gint tito = gwy_omp_chunk_end(ito+1 - ifrom) + ifrom;
        gdouble *ttarget = gwy_omp_if_threads_new0(target, nstats);
        gdouble *tweight = gwy_omp_if_threads_new0(weight, nstats);

        for (gint i = tifrom; i < tito; i++) {
            gdouble yy = (i + 0.5)*dy - y;
            for (gint j = jfrom; j <= jto; j++) {
                gdouble xx = (j + 0.5)*dx - x;
                gdouble v = d[i*xres + j];
                gdouble rr;
                gint kk;

                if (!nielded_included(m + i*xres + j, masking))
                    continue;

                rr = sqrt(xx*xx + yy*yy)/h;
                kk = floor(rr);
                if (kk+1 >= nstats) {
                    if (kk+1 == nstats) {
                        ttarget[kk] += v;
                        tweight[kk] += 1.0;
                    }
                    continue;
                }

                rr -= kk;
                if (rr <= 0.5)
                    rr = 2.0*rr*rr;
                else
                    rr = 1.0 - 2.0*(1.0 - rr)*(1.0 - rr);

                ttarget[kk] += (1.0 - rr)*v;
                ttarget[kk+1] += rr*v;
                tweight[kk] += 1.0 - rr;
                tweight[kk+1] += rr;
            }
        }

        gwy_omp_if_threads_sum_double(weight, tweight, nstats);
        gwy_omp_if_threads_sum_double(target, ttarget, nstats);
    }

    /* Get rid of initial and trailing no-data segment. */
    gint kfrom, kto;
    for (kfrom = 0; kfrom < nstats; kfrom++) {
        if (weight[kfrom])
            break;
    }
    for (kto = nstats-1; kto > kfrom; kto--) {
        if (weight[kto])
            break;
    }
    if (kto - kfrom < 2) {
        /* XXX: This is not correct.  We do not care. */
        line->real = h;
        target[0] = gwy_field_get_dval_real(field, x, y, GWY_INTERPOLATION_ROUND);
        return line;
    }

    if (kfrom != 0 || kto != nstats-1) {
        nstats = kto+1 - kfrom;
        gwy_line_crop(line, kfrom, nstats);
        target = line->priv->data;
        line->off = kfrom*h;
        memmove(weight, weight + kfrom, nstats*sizeof(gdouble));
    }
    g_assert(weight[0]);
    g_assert(weight[nstats-1]);

    /* Fill holes where we have no weight, this can occur near the start if large nstats is requested. */
    kfrom = -1;
    for (gint k = 0; k < nstats; k++) {
        if (weight[k]) {
            target[k] /= weight[k];
            if (kfrom+1 != k) {
                gdouble first = target[kfrom];
                gdouble last = target[k];
                gint j;
                for (j = kfrom+1; j < k; j++) {
                    gdouble w = (j - kfrom)/(gdouble)(k - kfrom);
                    target[j] = w*last + (1.0 - w)*first;
                }
            }
            kfrom = k;
        }
    }

    g_free(weight);

    return line;
}

/**************************************************************************
 *
 * Masked ACF, HHCF, PSDF
 * DOI 10.1016/j.ultramic.2012.08.002
 *
 **************************************************************************/

static void
row_assign_mask(GwyNield *mask,
                guint col,
                guint row,
                guint width,
                GwyMaskingType masking,
                gdouble *out)
{
    const gint *m = mask->priv->data + row*mask->xres + col;

    for (guint j = width; j; j--, out++, m++)
        nielded_included(m, masking);
}

static void
row_accumulate(gdouble *accum,
               const gdouble *data,
               guint size)
{
    guint j;

    for (j = size; j; j--, accum++, data++)
        *accum += *data;
}

/* FFTW calculates unnormalised DFT so we divide the result of the first transformation with (1/√size)² = 1/size and
 * keep the second transfrom as-is to obtain exactly g_k.
 *
 * Here we deviate from the paper and try to smoothly interpolate the missing values to reduce spurious high-frequency
 * content.  It helps sometimes... */
static void
row_divide_nonzero_with_laplace(const gdouble *numerator,
                                const gdouble *denominator,
                                gdouble *out,
                                guint size, guint thresh)
{
    GwyLine *line, *mask;
    guint j;
    gboolean have_zero = FALSE;

    for (j = 0; j < size; j++) {
        if (denominator[j] > thresh)
            out[j] = numerator[j]/denominator[j];
        else {
            out[j] = 0.0;
            have_zero = TRUE;
        }
    }

    if (!have_zero)
        return;

    line = gwy_line_new(size, size, FALSE);
    gwy_assign(line->priv->data, out, size);
    mask = gwy_line_new(size, size, FALSE);
    gdouble *m = mask->priv->data;
    for (j = 0; j < size; j++)
        m[j] = (denominator[j] == 0);

    gwy_line_laplace_solve(line, mask);
    gwy_assign(out, line->priv->data, size);

    g_object_unref(line);
    g_object_unref(mask);
}

static void
row_accum_cnorm(gdouble *accum,
                const fftw_complex *fftc,
                guint size,
                gdouble q)
{
    gdouble *out = accum, *out2 = accum + (size-1);

    q /= size;
    *out += q*gwy_cnorm(*fftc);
    out++, fftc++;
    for (guint j = (size + 1)/2 - 1; j; j--, fftc++, out++, out2--) {
        gdouble v = q*gwy_cnorm(*fftc);
        *out += v;
        *out2 += v;
    }
    if (size % 2 == 0)
        *out += q*gwy_cnorm(*fftc);
}

static void
row_extfft_accum_cnorm(fftw_plan plan,
                       gdouble *fftr,
                       gdouble *accum,
                       fftw_complex *fftc,
                       guint size,
                       guint width,
                       gdouble q)
{
    gwy_clear(fftr + width, size - width);
    gwy_fftw_execute(plan);
    row_accum_cnorm(accum, fftc, size, q);
}

/* Calculate the product A*B+AB*, equal to 2*(Re A Re B + Im A Im B), of two R2HC outputs (the result is added to @out
 * including the redundant even terms). */
static void
row_accum_cprod(const fftw_complex *fftca,
                const fftw_complex *fftcb,
                gdouble *out,
                guint size,
                gdouble q)
{
    gdouble *out2 = out + size-1;

    q *= 2.0/size;
    *out += q*gwy_csprod(*fftca, *fftcb);
    out++, fftca++, fftcb++;
    for (guint j = (size + 1)/2 - 1; j; j--, out++, fftca++, fftcb++, out2--) {
        gdouble v = q*gwy_csprod(*fftca, *fftcb);
        *out += v;
        *out2 += v;
    }
    if (size % 2 == 0)
        *out += q*gwy_csprod(*fftca, *fftcb);
}

/* Used in cases when we expect the imaginary part to be zero but do not want to bother with specialised DCT. */
static void
row_extfft_extract_re(fftw_plan plan,
                      gdouble *fftr,
                      gdouble *out,
                      fftw_complex *fftc,
                      guint size,
                      guint width)
{
    gwy_assign(fftr, out, size);
    gwy_fftw_execute(plan);
    for (guint j = 0; j < width; j++)
        out[j] = creal(fftc[j]);
}

static void
row_extfft_symmetrise_re(fftw_plan plan,
                         gdouble *fftr,
                         gdouble *out,
                         fftw_complex *fftc,
                         guint size)
{
    gdouble *out2 = out + size-1;

    gwy_assign(fftr, out, size);
    gwy_fftw_execute(plan);

    *out = creal(*fftc);
    out++, fftc++;
    for (guint j = (size + 1)/2 - 1; j; j--, fftc++, out++, out2--)
        *out = *out2 = creal(*fftc);
    if (size % 2 == 0)
        *out = creal(*fftc);
}

static void
row_accumulate_vk(const gdouble *data,
                  gdouble *v,
                  guint size)
{
    const gdouble *data2 = data + (size-1);
    gdouble sum = 0.0;
    guint j;

    v += size-1;
    for (j = size; j; j--, data++, data2--, v--) {
        sum += (*data)*(*data) + (*data2)*(*data2);
        *v += sum;
    }
}

/* Level a row of data by subtracting the mean value. */
static void
row_level1(const gdouble *in,
           gdouble *out,
           guint n)
{
    gdouble avg = gwy_math_mean(in, n);
    for (guint i = 0; i < n; i++)
        out[i] = in[i] - avg;
}

static void
row_level2(const gdouble *in,
           gdouble *out,
           guint n)
{
    if (n < 2) {
        gwy_clear(out, n);
        return;
    }

    gdouble coeffs[2], x0 = -0.5*(n - 1);
    gwy_math_fit_poly_equidist(in, n, x0, 1.0, 1, coeffs);
    gdouble a = coeffs[0], b = coeffs[1];

    for (gint i = 0; i < n; i++)
        out[i] = in[i] - a - b*(i + x0);
}

/* Level a row of data by subtracting the mean value of data under mask and clear (set to zero) all data not under
 * mask.  Note how the zeroes nicely ensure that the subsequent functions Just Work(TM) and don't need to know we use
 * masking at all. */
static guint
row_level1_mask(const gdouble *in,
                gdouble *out,
                guint n,
                const gint *m,
                GwyMaskingType masking)
{
    gdouble sumz = 0.0, a;
    const gdouble *pdata = in;
    const gint *mdata = m;
    guint nd = 0;

    for (guint i = n; i; i--, pdata++, mdata++) {
        guint c = nielded_included(m, masking);
        gdouble z = *pdata;
        sumz += c*z;
        nd += c;
    }

    if (!nd) {
        gwy_clear(out, n);
        return nd;
    }

    a = sumz/nd;
    pdata = in;
    mdata = m;
    for (guint i = n; i; i--, pdata++, mdata++, out++) {
        guint c = nielded_included(m, masking);
        gdouble z = *pdata;
        *out = c*(z - a);
    }

    return nd;
}

static guint
row_level2_mask(const gdouble *in,
                gdouble *out,
                guint n,
                const gint *m,
                GwyMaskingType masking)
{
    gdouble sumx = 0.0, sumxx = 0.0, sumz = 0.0, sumxz = 0.0, a, b;
    const gdouble *pdata = in;
    const gint *mdata = m;
    guint nd = 0;

    for (guint i = n; i; i--, pdata++, mdata++) {
        guint c = nielded_included(mdata, masking);
        gdouble z = c*(*pdata);
        gdouble x = c*(i - 0.5*n);
        sumz += z;
        sumxz += x*z;
        sumx += x;
        sumxx += x*x;
        nd += c;
    }

    if (nd < 2) {
        gwy_clear(out, n);
        return nd;
    }

    gdouble matrix[3] = { nd, sumx, sumxx }, rhs[2] = { sumz, sumxz };
    gwy_math_choleski_decompose(2, matrix);
    gwy_math_choleski_solve(2, matrix, rhs);
    a = rhs[0];
    b = rhs[1];

    pdata = in;
    mdata = m;
    for (guint i = n; i; i--, pdata++, mdata++, out++) {
        guint c = nielded_included(mdata, masking);
        gdouble z = *pdata;
        gdouble x = i - 0.5*n;
        *out = (z - a - b*x)*c;
    }

    return nd;
}

/* Window a row using a sampled windowing function. */
static void
row_window(gdouble *data, const gdouble *window, guint n)
{
    guint i;

    for (i = n; i; i--, data++, window++)
        *data *= *window;
}

/* Level and count the number of valid data in a row */
static guint
row_level_and_count(const gdouble *in,
                    gdouble *out,
                    guint width,
                    GwyNield *mask,
                    GwyMaskingType masking,
                    guint maskcol,
                    guint maskrow,
                    guint level)
{
    if (!mask || masking == GWY_MASK_IGNORE) {
        if (level > 1)
            row_level2(in, out, width);
        else if (level)
            row_level1(in, out, width);
        else
            gwy_assign(out, in, width);
        return width;
    }

    const gint *m = mask->priv->data + mask->xres*maskrow + maskcol;
    if (level > 1)
        return row_level2_mask(in, out, width, m, masking);
    else
        return row_level1_mask(in, out, width, m, masking);

    guint count = 0;
    for (guint i = width; i; i--, in++, out++, m++) {
        guint c = nielded_included(m, masking);
        *out = c*(*in);
        count += c;
    }

    return count;
}

static void
set_cf_units(GwyField *field,
             GwyLine *line,
             GwyLine *weights)
{
    gwy_field_copy_units_to_line(field, line);
    GwyLinePrivate *priv = line->priv;
    if (priv->unit_y)
        gwy_unit_power(priv->unit_y, priv->unit_y, 2);

    if (weights) {
        gwy_field_copy_units_to_line(field, weights);
        gwy_unit_clear(gwy_line_get_unit_y(weights));
    }
}

/**
 * gwy_field_area_row_acf:
 * @field: A two-dimensional data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @level: The first polynomial degree to keep in the rows, lower degrees than @level are subtracted.
 * @weights: (nullable):
 *           Line to store the denominators to (or %NULL).  It will be resized to match the returned line.  The
 *           denominators are integers equal to the number of terms that contributed to each value.  They are suitable
 *           as fitting weights if the ACF is fitted.
 *
 * Calculates the row-wise autocorrelation function (ACF) of a field.
 *
 * The calculated ACF has the natural number of points, i.e. @width.
 *
 * Masking is performed by omitting all terms that contain excluded pixels. Since different rows contain different
 * numbers of pixels, the resulting ACF values are calculated as a weighted sums where weight of each row's
 * contribution is proportional to the number of contributing terms.  In other words, the weighting is fair: each
 * contributing pixel has the same influence on the result.
 *
 * Only @level values 0 (no levelling) and 1 (subtract the mean value) and 2 (subtract the mean plane) are currently
 * available. For SPM data, you usually wish to pass 1.
 *
 * Returns: (transfer full): A new one-dimensional data line with the ACF.
 **/
GwyLine*
gwy_field_area_row_acf(GwyField *field,
                       GwyNield *mask,
                       GwyMaskingType masking,
                       guint col, guint row,
                       guint width, guint height,
                       guint level,
                       GwyLine *weights)
{
    GwyLine *line = gwy_line_new(width, 1.0, TRUE);

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return line;
    g_return_val_if_fail(!weights || GWY_IS_LINE(weights), line);

    guint nfullrows = 0, nemptyrows = 0;

    /* Transform size must be at least twice the data size for zero padding. An even size is necessary due to
     * alignment constraints in FFTW. Using this size for all buffers is a bit excessive but safe. */
    guint size = gwy_fft_find_nice_size((width + 1)/2*4);
    /* The innermost (contiguous) dimension of R2C the complex output is slightly larger than the real input.  Note
     * @cstride is measured in fftw_complex, multiply it by 2 for doubles. */
    guint cstride = size/2 + 1;
    const gdouble *base = field->priv->data + row*field->xres + col;
    gdouble *fftr = fftw_alloc_real(size);
    gdouble *accum_data = g_new(gdouble, 2*size);
    gdouble *accum_mask = accum_data + size;
    fftw_complex *fftc = fftw_alloc_complex(cstride);
    fftw_plan plan = gwy_fftw_plan_dft_r2c_1d(size, fftr, fftc, FFTW_DESTROY_INPUT | FFTW_ESTIMATE);
    gwy_clear(accum_data, size);
    gwy_clear(accum_mask, size);

    /* Gather squared Fourier coefficients for all rows. */
    for (guint i = 0; i < height; i++) {
        guint count = row_level_and_count(base + i*field->xres, fftr, width, mask, masking, col, row + i, level);
        if (!count) {
            nemptyrows++;
            continue;
        }

        /* Calculate and gather squared Fourier coefficients of the data. */
        row_extfft_accum_cnorm(plan, fftr, accum_data, fftc, size, width, 1.0);

        if (count == width) {
            nfullrows++;
            continue;
        }

        /* Calculate and gather squared Fourier coefficients of the mask. */
        row_assign_mask(mask, col, row + i, width, masking, fftr);
        row_extfft_accum_cnorm(plan, fftr, accum_mask, fftc, size, width, 1.0);
    }

    /* Numerator of G_k, i.e. FFT of squared data Fourier coefficients. */
    row_extfft_extract_re(plan, fftr, accum_data, fftc, size, width);

    /* Denominator of G_k, i.e. FFT of squared mask Fourier coefficients. Don't perform the FFT if there were no
     * partial rows. */
    if (nfullrows + nemptyrows < height)
        row_extfft_extract_re(plan, fftr, accum_mask, fftc, size, width);

    for (guint j = 0; j < width; j++) {
        /* Denominators must be rounded to integers because they are integers and this permits to detect zeroes in the
         * denominator. */
        accum_mask[j] = GWY_ROUND(accum_mask[j]) + nfullrows*(width - j);
    }
    row_divide_nonzero_with_laplace(accum_data, accum_mask, line->priv->data, line->res, GWY_ROUND(log(width*height)));

    line->real = gwy_field_get_dx(field)*line->res;
    /* line->off = -0.5*line->real/line->res; */

    if (weights) {
        gwy_line_resize(weights, line->res);
        gwy_line_set_real(weights, line->real);
        gwy_line_set_offset(weights, line->off);
        gwy_assign(weights->priv->data, accum_mask, weights->res);
    }

    fftw_destroy_plan(plan);
    fftw_free(fftc);
    g_free(accum_data);
    fftw_free(fftr);

    set_cf_units(field, line, weights);
    return line;
}

static void
normalise_window_square(gdouble *window, guint n)
{
    gdouble s = 0.0;
    guint i;

    for (i = 0; i < n; i++)
        s += window[i]*window[i];

    if (!s)
        return;

    s = sqrt(n/s);
    for (i = 0; i < n; i++)
        window[i] *= s;
}

/**
 * gwy_field_area_row_psdf:
 * @field: A two-dimensional data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @windowing: Windowing type to use.
 * @level: The first polynomial degree to keep in the rows; lower degrees than @level are subtracted.
 *
 * Calculates the row-wise power spectral density function (PSDF) of a rectangular part of a field.
 *
 * The calculated PSDF has the natural number of points that follows from DFT, i.e. @width/2+1.
 *
 * The reduction of the total energy by windowing is compensated by multiplying the PSDF to make its sum of squares
 * equal to the input data sum of squares.
 *
 * Masking is performed by omitting all terms that contain excluded pixels. Since different rows contain different
 * numbers of pixels, the resulting PSDF is calculated as a weighted sum where each row's weight is proportional to
 * the number of contributing pixels.  In other words, the weighting is fair: each contributing pixel has the same
 * influence on the result.
 *
 * Only @level values 0 (no levelling) and 1 (subtract the mean value) and 2 (subtract the mean plane) are currently
 * available. For SPM data, you usually wish to pass 1.
 *
 * Do not assume the PSDF values are all positive, when masking is in effect. The PSDF should still have the correct
 * integral, but it will be contaminated with noise, both positive and negative.
 *
 * Returns: (transfer full): A new one-dimensional data line with the PSDF.
 **/
GwyLine*
gwy_field_area_row_psdf(GwyField *field,
                        GwyNield *mask,
                        GwyMaskingType masking,
                        guint col, guint row,
                        guint width, guint height,
                        GwyWindowingType windowing,
                        guint level)
{
    GwyLine *line = gwy_line_new(width, 1.0, TRUE);

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return line;

    fix_levelling_degree(&level);

    guint nfullrows = 0, nemptyrows = 0;

    /* The innermost (contiguous) dimension of R2C the complex output is
     * slightly larger than the real input.  Note @cstride is measured in
     * fftw_complex, multiply it by 2 for doubles. */
    guint cstride = width/2 + 1;
    gwy_line_resize(line, cstride);
    /* An even size is necessary due to alignment constraints in FFTW.
     * Using this size for all buffers is a bit excessive but safe. */

    guint size = (width + 3)/4*4;
    const gdouble *base = field->priv->data + row*field->xres + col;
    gdouble *fftr = fftw_alloc_real(size);
    gdouble *accum_data = g_new(gdouble, 2*size + width);
    gdouble *accum_mask = accum_data + size;
    gdouble *window = accum_data + 2*size;
    fftw_complex *fftc = fftw_alloc_complex(cstride);

    gwy_clear(accum_data, size);
    gwy_clear(accum_mask, size);
    gwy_math_fill(window, width, 1.0);
    gwy_fft_window(width, window, windowing);
    normalise_window_square(window, width);
    fftw_plan plan = gwy_fftw_plan_dft_r2c_1d(width, fftr, fftc, FFTW_DESTROY_INPUT | FFTW_ESTIMATE);

    for (guint i = 0; i < height; i++) {
        guint count = row_level_and_count(base + i*field->xres, fftr, width, mask, masking, col, row + i, level);
        if (!count) {
            nemptyrows++;
            continue;
        }

        /* Calculate and gather squared Fourier coefficients of the data. */
        row_window(fftr, window, width);
        row_extfft_accum_cnorm(plan, fftr, accum_data, fftc, width, width, 1.0);

        if (count == width) {
            nfullrows++;
            continue;
        }

        /* Calculate and gather squared Fourier coefficients of the mask. */
        row_assign_mask(mask, col, row + i, width, masking, fftr);
        row_extfft_accum_cnorm(plan, fftr, accum_mask, fftc, width, width, 1.0);
    }

    /* Numerator of A_k, i.e. FFT of squared data Fourier coefficients. */
    row_extfft_symmetrise_re(plan, fftr, accum_data, fftc, width);

    /* Denominator of A_k, i.e. FFT of squared mask Fourier coefficients. Don't perform the FFT if there were no
     * partial rows. */
    if (nfullrows + nemptyrows < height)
        row_extfft_symmetrise_re(plan, fftr, accum_mask, fftc, width);

    for (guint j = 0; j < width; j++) {
        /* Denominators must be rounded to integers because they are integers and this permits to detect zeroes in the
         * denominator. */
        accum_mask[j] = GWY_ROUND(accum_mask[j]) + nfullrows*width;
    }
    row_divide_nonzero_with_laplace(accum_data, accum_mask, fftr, width, GWY_ROUND(log(width*height)));

    /* The transform is the other way round – for complex numbers.  Since it is in fact a DCT here we don't care and
     * run it as a forward transform. */
    gwy_fftw_execute(plan);
    for (guint j = 0; j < line->res; j++)
        line->priv->data[j] = creal(fftc[j]);

    fftw_destroy_plan(plan);
    fftw_free(fftc);
    fftw_free(fftr);
    g_free(accum_data);

    gwy_line_multiply(line, gwy_field_get_dx(field)/(2*G_PI));
    line->real = G_PI/gwy_field_get_dx(field);
    /* line->off = -0.5*line->real/line->res; */

    gwy_unit_power(gwy_line_get_unit_x(line), gwy_field_get_unit_xy(field), -1);
    gwy_unit_power_multiply(gwy_line_get_unit_y(line),
                            gwy_field_get_unit_xy(field), 1,
                            gwy_field_get_unit_z(field), 2);
    return line;
}

/**
 * gwy_field_area_row_hhcf:
 * @field: A two-dimensional data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @level: The first polynomial degree to keep in the rows, lower degrees than @level are subtracted.
 * @weights: (nullable):
 *           Line to store the denominators to (or %NULL).  It will be resized to match the returned line.  The
 *           denominators are integers equal to the number of terms that contributed to each value.  They are suitable
 *           as fitting weights if the HHCF is fitted.
 *
 * Calculates the row-wise height-height correlation function (HHCF) of a rectangular part of a field.
 *
 * The calculated HHCF has the natural number of points, i.e. @width.
 *
 * Masking is performed by omitting all terms that contain excluded pixels. Since different rows contain different
 * numbers of pixels, the resulting HHCF values are calculated as a weighted sums where weight of each row's
 * contribution is proportional to the number of contributing terms.  In other words, the weighting is fair: each
 * contributing pixel has the same influence on the result.
 *
 * Only @level values 0 (no levelling) and 1 (subtract the mean value) and 2 (subtract the mean plane) are currently
 * available. For SPM data, you usually wish to pass 1.
 *
 * Returns: (transfer full): A new one-dimensional data line with the HHCF.
 **/
GwyLine*
gwy_field_area_row_hhcf(GwyField *field,
                        GwyNield *mask,
                        GwyMaskingType masking,
                        guint col, guint row,
                        guint width, guint height,
                        guint level,
                        GwyLine *weights)
{
    GwyLine *line = gwy_line_new(width, 1.0, TRUE);

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return line;
    g_return_val_if_fail(!weights || GWY_IS_LINE(weights), line);

    fix_levelling_degree(&level);

    guint nfullrows = 0, nemptyrows = 0;

    /* Transform size must be at least twice the data size for zero padding.
     * An even size is necessary due to alignment constraints in FFTW.
     * Using this size for all buffers is a bit excessive but safe. */
    guint size = gwy_fft_find_nice_size((width + 1)/2*4);
    /* The innermost (contiguous) dimension of R2C the complex output is
     * slightly larger than the real input.  Note @cstride is measured in
     * fftw_complex, multiply it by 2 for doubles. */
    guint cstride = size/2 + 1;
    const gdouble *base = field->priv->data + row*field->xres + col;
    gdouble *fftr = fftw_alloc_real(size);
    gdouble *accum_data = g_new(gdouble, 3*size);
    gdouble *accum_mask = accum_data + size;
    gdouble *accum_v = accum_data + 2*size;
    fftw_complex *fftc = fftw_alloc_complex(cstride);
    fftw_complex *tmp = g_new(fftw_complex, cstride);
    fftw_plan plan = gwy_fftw_plan_dft_r2c_1d(size, fftr, fftc, FFTW_DESTROY_INPUT | FFTW_ESTIMATE);
    gwy_clear(accum_data, size);
    gwy_clear(accum_mask, size);
    gwy_clear(accum_v, size);

    // Gather V_ν-2|Z_ν|² for all rows, except that for full rows we actually gather just -2|Z_ν|² because v_k can be
    // calculated without DFT.
    for (guint i = 0; i < height; i++) {
        guint count = row_level_and_count(base + i*field->xres, fftr, width, mask, masking, col, row + i, level);
        if (!count) {
            nemptyrows++;
            continue;
        }

        /* Calculate v_k before FFT destroys the input levelled/filtered data. */
        if (count == width)
            row_accumulate_vk(fftr, accum_v, width);
        else {
            // For partial rows, we will need the data later to calculate FFT of their squares.  Save them to the line
            // that conveniently has the right size.
            gwy_assign(line->priv->data, fftr, width);
        }

        /* Calculate and gather -2 times squared Fourier coefficients. */
        row_extfft_accum_cnorm(plan, fftr, accum_data, fftc, size, width, -2.0);

        if (count == width) {
            nfullrows++;
            continue;
        }

        /* First calculate U_ν (Fourier cofficients of squared data).  Save them to tmp. */
        gdouble *q = line->priv->data;
        gdouble *p = fftr;
        for (guint j = width; j; j--, p++, q++)
            *p = (*q)*(*q);
        gwy_clear(fftr + width, size - width);
        gwy_fftw_execute(plan);
        gwy_assign(tmp, fftc, cstride);

        /* Mask.  We need the intermediate result C_ν to combine it with U_ν. */
        row_assign_mask(mask, col, row + i, width, masking, fftr);
        gwy_clear(fftr + width, size - width);
        gwy_fftw_execute(plan);

        /* Accumulate V_ν (calculated from C_ν and U_ν) to accum_data. */
        row_accum_cprod(tmp, fftc, accum_data, size, 1.0);

        /* And accumulate squared mask Fourier coeffs |C_ν|². */
        row_accum_cnorm(accum_mask, fftc, size, 1.0);
    }

    /* Numerator of H_k, excluding non-DFT data in v_k. */
    row_extfft_extract_re(plan, fftr, accum_data, fftc, size, width);
    /* Combine it with v_k to get the full numerator in accum_data. */
    row_accumulate(accum_data, accum_v, width);

    // Denominator of H_k, i.e. FFT of squared mask Fourier coefficients. Don't perform the FFT if there were no
    // partial rows.
    if (nfullrows + nemptyrows < height)
        row_extfft_extract_re(plan, fftr, accum_mask, fftc, size, width);

    for (guint j = 0; j < width; j++) {
        // Denominators must be rounded to integers because they are integers and this permits to detect zeroes in the
        // denominator.
        accum_mask[j] = GWY_ROUND(accum_mask[j]) + nfullrows*(width - j);
    }
    row_divide_nonzero_with_laplace(accum_data, accum_mask, line->priv->data, line->res, GWY_ROUND(log(width*height)));

    line->real = gwy_field_get_dx(field)*line->res;
    /* line->off = -0.5*line->real/line->res; */

    if (weights) {
        gwy_line_resize(weights, line->res);
        gwy_line_set_real(weights, line->real);
        gwy_line_set_offset(weights, line->off);
        gwy_assign(weights->priv->data, accum_mask, weights->res);
    }

    fftw_destroy_plan(plan);
    fftw_free(fftc);
    fftw_free(fftr);
    g_free(accum_data);
    g_free(tmp);

    set_cf_units(field, line, weights);
    return line;
}

/* Recalculate area excess based on second-order expansion to the true one, assuming the distribution is exponential.
 * */
static inline gdouble
asg_correction(gdouble ex)
{
    if (ex < 1e-3)
        return ex*(1.0 - ex*(1.0 - 3.0*ex*(1.0 - 5.0*ex*(1.0 - 7.0*ex*(1.0 - 9.0*ex*(1.0 - 11.0*ex))))));

    return sqrt(0.5*G_PI*ex) * exp(0.5/ex) * erfc(sqrt(0.5/ex));
}

/**
 * gwy_field_area_row_asg:
 * @field: A two-dimensional data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @level: The first polynomial degree to keep in the rows, lower degrees than @level are subtracted.
 *
 * Calculates the row-wise area scale graph (ASG) of a rectangular part of a field.
 *
 * The calculated ASG has the natural number of points, i.e. @width-1.
 *
 * The ASG represents the apparent area excess (ratio of surface and projected area minus one) observed at given
 * length scale.  The quantity calculated by this function serves a similar purpose as ASME B46.1 area scale graph but
 * is defined differently, based on the HHCF.  See gwy_field_area_row_hhcf() for details of its calculation.
 *
 * Only @level values 0 (no levelling) and 1 (subtract the mean value) and 2 (subtract the mean plane) are currently
 * available. For SPM data, you usually wish to pass 1.
 *
 * Returns: (transfer full): A new one-dimensional data line with the ASG.
 **/
GwyLine*
gwy_field_area_row_asg(GwyField *field,
                       GwyNield *mask,
                       GwyMaskingType masking,
                       guint col, guint row,
                       guint width, guint height,
                       guint level)
{
    GwyLine *hhcf = gwy_field_area_row_hhcf(field, mask, masking, col, row, width, height, level, NULL);
    g_return_val_if_fail(hhcf, NULL);

    guint res = hhcf->res;
    gdouble dx = hhcf->real/res;
    if (hhcf->res < 2) {
        GwyLine *line = gwy_line_new(1, dx, TRUE);
        g_object_unref(hhcf);
        return line;
    }

    res--;
    GwyLine *line = line = gwy_line_new(res, dx*res, FALSE);
    line->off = 0.5*dx;
    GwyLinePrivate *lpriv = line->priv;

    for (guint i = 0; i < res; i++) {
        gdouble t = (i + 0.5)*dx + line->off;
        lpriv->data[i] = asg_correction(hhcf->priv->data[i+1]/(t*t));
    }

    _gwy_copy_unit(field->priv->unit_xy, &lpriv->unit_x);
    g_object_unref(hhcf);

    return line;
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
