Fourier Part 2: Integrating a discontinuous function

Checking if gsl_integration_qng() will work

At the end of part 1, I suggested that gsl_integraton_qng() would not be able to successfully integrate the function we want to find the Fourier series of. Here is the function I will find the Fourier series of.

fourier original

Because this function is discontinuous, we have to approximate it with a Fourier series instead of a Taylor series. If we recall the formulas shown in the last part, we know that we will have to integrate \( f(x) \) from \( 0 \) to \(2l\), among other things. Before calculating the Fourier series, let's try using gsl_integration_qng() to perform basic integration.

  #include <stdio.h>
  #include <math.h>
  #include <gsl/gsl_integration.h>

  /* f(x) = x for 0 <= x < 4, repeating */
  double f(double x, void *params) {
    const double period = 4;
    return fmod(x, period);
  }

  int main() {
    double result, error;
    double low = 0, high = 8;
    gsl_function F = {.function = &f};
    double err_abs = 0, err_rel = 1;
    size_t num_evals;

    gsl_integration_qng(&F, low, high,
            err_abs, err_rel,
            &result, &error, &num_evals);
    printf("result error num_evals\n");
    printf("%f %f %zu\n", result, error, num_evals);
  }
|    result |    error | num_evals |
| 14.804436 | 8.015135 |        21 |

Well, something happened here, but it's not quite what we wanted. We told GSL to take \( \int_0^8 f(x) dx \). Since we're dealing with a discontinuous function, we can rewrite this as \( \int_0^4 x dx + \int_4^8 x dx \) Now I'm no mathematician, but this answer should be 16, not 14.8. Additionally, our error is very large, and if I attempt to lower err_rel to a smaller value, GSL yells at me saying that it failed to reach the requested error.

This is not a problem on GSL's end. We aren't using the right integration function for the job! According to the GSL manual,

The QNG algorithm is a non-adaptive procedure which uses fixed Gauss-Kronrod-Patterson abscissae to sample the integrand at a maximum of 87 points. It is provided for fast integration of smooth functions.

While that's a lot of words, the important part is the last sentence. gsl_integration_qng() is for smooth functions. Our function is not smooth, it has a discontinuity! We will need to find an alternative function in GSL that can handle discontinuous graphs.[fn:1:Strictly speaking, this isn't actually true. Because the discontinuity occurs at the limits of integration for our function (as long as we only integrate from 0 to 4), we can use gsl_integration_qng(). Not all functions are like this, so it's still best to find an alternative.]

gsl_integration_qag() and discontinuities

Fortunately we do not have to look far. The very next function mentioned in the manual is what we need. gsl_integration_qag() has the following signature.

  int gsl_integration_qag(const gsl_function * f,
              double a, double b,
              double epsabs, double epsrel,
              size_t limit, int key,
              gsl_integration_workspace * workspace,
              double * result, double * abserr)

f, a, b, epsabs, epsrel, result, and abserr are all the same as gsl_integration_qng(). We can see that several new terms are introduced however.

workspace is a pointer to an area of memory used by gsl_integration_qag(). We allocate space by using the gsl_integration_workspace_alloc() function, This function is passed an integer to adjust how much memory we want to allocate. Whatever integer we pass to gsl_integration_workspace_alloc(), we need to ensure that limit is the same. This way, gsl_integration_qag() knows how much memory it has available.

key is a integer between 0 through 6. GSL recommends using higher values when integrating smooth functions, and lower values when functions are discontinuous.

Let's see how well gsl_integration_qag() performs!

  #include <stdio.h>
  #include <math.h>
  #include <gsl/gsl_integration.h>

  double f(double x, void *params) {
    const double period = 4;
    return fmod(x, period);
  }

  int main() {
    size_t limit = 1024;
    gsl_integration_workspace *w
      = gsl_integration_workspace_alloc(limit);
    double result, error;
    double low = 0, high = 8;
    gsl_function F = {.function = &f};
    double err_abs = 0, err_rel = 1e-7;

    gsl_integration_qag(&F, low, high,
            err_abs, err_rel,
            limit, 1, w,
            &result, &error);
    /* get into the habit of freeing memory when done! */
    gsl_integration_workspace_free(w);
    printf("result error\n");
    printf("%f %f\n", result, error);
  }
| result | error |
|   16.0 |   0.0 |

Look at that! We are getting the exact value we expected, even though we're integrating with a discontinuity. We are now at the point where we can calculate the Fourier series for our function.

Generating our Fourier series

Let's recall that we can find our \(a_n\) and \(b_n\) coefficients with the following formulas.

\[ a_n = \frac{1}{l} \int_{0}^{2l} f(x) \cos(\frac{n \pi x }{l}) dx \]

\[ b_n = \frac{1}{l} \int_{0}^{2l} f(x) \sin(\frac{n \pi x }{l}) dx \]

GSL expects us to only pass one function to it as an argument. That means that, unlike before, we can't just pass a pointer to our function f(x), as GSL won't be multiplying it by the cosine and sine terms.

There are a couple of solutions to this that I can think of. The first is to make use of the void * argument that GSL includes in the gsl_function structure. Using this, we could pass extra information to f(x), adapting it to our specific n value.

Alternatively, we could write a parent function, like aorb_subn(). This function would then be stored in the gsl_function structure. The advantage of this approach is that the function we want the Fourier series of, f(x), is distinct in our code. This should make it a bit easier to change f(x) to any function that we want. This is the approach that I will take.

To make this code hopefully a bit more modular, I moved the integration out of main, and instead into a function called get_aorb_subn(). This function calculates the nth Fourier coefficient for a function f(x), using the variable get_a to determine if it should calculate \( a_n \) or \( b_n \). It may be better to wrap this in a fourier structure that contains the two terms, but I elected not to do that to hopefully maintain some vague semblance of readability.

This is what the code to calculate the Fourier series looks like.

  #include <stdbool.h>
  #include <stdio.h>
  #include <math.h>
  #include <gsl/gsl_integration.h>

  #define PI 3.14159
  const double period = 4;
  const double l = period / 2;

  struct aorb_params {
    double (*f)(double x, void *params);
    int n;
    bool calc_a;
  };

  double f(double x, void *params) {
    return fmod(x, period);
  }

  double aorb_subn(double x, struct aorb_params *params) {
    int n = params->n;
    double trig = params->calc_a ? cos(PI*n*x/l) : sin(PI*n*x/l);
    return params->f(x, NULL) * trig;
  }

  double get_aorb_subn(double (*f)(double x, void *params), int n, 
               double low, double high, bool get_a) {
    double normalization = 1/l;
    if (get_a && n == 0) normalization /= 2;

    size_t limit = 1024;
    gsl_integration_workspace *w
      = gsl_integration_workspace_alloc(limit);
    double result, error;
    gsl_function F = { .function = &aorb_subn,
      .params = &(struct aorb_params){ .f = f, .n = n,
             .calc_a = get_a } };
    double err_abs = 0, err_rel = 1e-7;

    gsl_integration_qag(&F, low, high,
            err_abs, err_rel,
            limit, 1, w,
            &result, &error);
    gsl_integration_workspace_free(w);

    return normalization * result;
  }

  int main() {
    const int upto = 10;
    double a_subn[upto], b_subn[upto];
    double low = 0, high = period;

    for (int i = 0; i < upto; i++) {
      a_subn[i] = get_aorb_subn(f, i, low, high, true);
      b_subn[i] = get_aorb_subn(f, i, low, high, false);
      printf("%d %f %f\n", i, a_subn[i], b_subn[i]);
    }
  }
| n | a_subn |    b_subn |
|---+--------+-----------|
| 0 |    2.0 |       0.0 |
| 1 | -7e-06 | -1.273242 |
| 2 | -7e-06 | -0.636621 |
| 3 | -7e-06 | -0.424414 |
| 4 | -7e-06 |  -0.31831 |
| 5 | -7e-06 | -0.254648 |
| 6 | -7e-06 | -0.212207 |
| 7 | -7e-06 | -0.181892 |
| 8 | -7e-06 | -0.159155 |
| 9 | -7e-06 | -0.141471 |

Like I mentioned before, aorb_subn is a "parent function" that combines f(x) and the sine/cosine term. This is the function we integrate, and we provide it enough information to know what term we are calculating.

Interestingly, if we use the \( \pi \) constant M_PI, the integration fails due to roundoff error. Fortunately we can get "close enough" values by just using a few less digits.

Once we graph the Fourier series generated by these coefficients, this is what we get.

fourier graph custom coefficients

And there we are! Here is a Fourier series generated for a discontinuous periodic function. The more terms we add, the more accurate the approximation.