2

I have a problem that, after much head scratching, I think is to do with very small numbers in a long-double.

I am trying to implement Planck's law equation to generate a normalised blackbody curve at 1nm intervals between a given wavelength range and for a given temperature. Ultimately this will be a function accepting inputs, for now it is main() with the variables fixed and outputting by printf().

I see examples in matlab and python, and they are implementing the same equation as me in a similar loop with no trouble at all.

This is the equation:

Plancks Law

My code generates an incorrect blackbody curve: Blackbody code incorrect output

I have tested key parts of the code independently. After trying to test the equation by breaking it into blocks in excel I noticed that it does result in very small numbers and I wonder if my implementation of large numbers could be causing the issue? Does anyone have any insight into using C to implement equations? This a new area to me and I have found the maths much harder to implement and debug than normal code.

#include <stdio.h>
#include <math.h>
#include <stdlib.h>  

//global variables
const double H = 6.626070040e-34; //Planck's constant (Joule-seconds)
const double C = 299800000;       //Speed of light in vacume (meters per second)
const double K = 1.3806488e-23;   //Boltzmann's constant (Joules per Kelvin)
const double nm_to_m = 1e-6;      //conversion between nm and m
const int interval = 1;           //wavelength interval to caculate at (nm)

//typedef structure to hold results
typedef struct {
    int *wavelength;
    long double *radiance;
    long double *normalised;
} results;

int main() {
    int min = 100 , max = 3000;            //wavelength bounds to caculate between, later to be swaped to function inputs
    double temprature = 200;               //temprature in kelvin, later to be swaped to function input
    double new_valu, old_valu = 0;
    static results SPD_data, *SPD;         //setup a static results structure and a pointer to point to it
    SPD = &SPD_data;
    SPD->wavelength = malloc(sizeof(int) * (max - min));          //allocate memory based on wavelength bounds
    SPD->radiance = malloc(sizeof(long double) * (max - min));
    SPD->normalised = malloc(sizeof(long double) * (max - min));

    for (int i = 0; i <= (max - min); i++) {
        //Fill wavelength vector
        SPD->wavelength[i] = min + (interval * i);

        //Computes radiance for every wavelength of blackbody of given temprature
        SPD->radiance[i] = ((2 * H * pow(C, 2)) / (pow((SPD->wavelength[i] / nm_to_m), 5))) * (1 / (exp((H * C) / ((SPD->wavelength[i] / nm_to_m) * K * temprature))-1));

        //Copy SPD->radiance to SPD->normalised
        SPD->normalised[i] = SPD->radiance[i];

        //Find largest value
        if (i <= 0) {
            old_valu = SPD->normalised[0];
        } else if (i > 0){
            new_valu = SPD->normalised[i];
            if (new_valu > old_valu) {
                old_valu = new_valu;
            }
        }
    }
    //for debug perposes
    printf("wavelength(nm)  radiance(Watts per steradian per meter squared)  normalised radiance\n");

    for (int i = 0; i <= (max - min); i++) {
        //Normalise SPD
        SPD->normalised[i] = SPD->normalised[i] / old_valu;

        //for debug perposes
        printf("%d %Le %Lf\n", SPD->wavelength[i], SPD->radiance[i], SPD->normalised[i]);
    }
    return 0; //later to be swaped to 'return SPD';
}

/*********************UPDATE Friday 24th Mar 2017 23:42*************************/

Thank you for the suggestions so far, lots of useful pointers especially understanding the way numbers are stored in C (IEEE 754) but I don't think that is the issue here as it only applies to significant digits. I implemented most of the suggestions but still no progress on the problem. I suspect Alexander in the comments is probably right, changing the units and order of operations is likely what I need to do to make the equation work like the matlab or python examples, but my knowledge of maths is not good enough to do this. I broke the equation down into chunks to take a closer look at what it was doing.

//global variables
const double H = 6.6260700e-34;   //Planck's constant (Joule-seconds) 6.626070040e-34
const double C = 299792458;         //Speed of light in vacume (meters per second)
const double K = 1.3806488e-23;     //Boltzmann's constant (Joules per Kelvin) 1.3806488e-23
const double nm_to_m = 1e-9;        //conversion between nm and m
const int interval = 1;             //wavelength interval to caculate at (nm)
const int min = 100, max = 3000;    //max and min wavelengths to caculate between (nm)
const double temprature = 200;      //temprature (K)

//typedef structure to hold results
typedef struct {
    int *wavelength;
    long double *radiance;
    long double *normalised;
} results;

//main program
int main()
{
    //setup a static results structure and a pointer to point to it
    static results SPD_data, *SPD;
    SPD = &SPD_data;
    //allocate memory based on wavelength bounds
    SPD->wavelength = malloc(sizeof(int) * (max - min));
    SPD->radiance = malloc(sizeof(long double) * (max - min));
    SPD->normalised = malloc(sizeof(long double) * (max - min));

    //break equasion into visible parts for debuging
    long double aa, bb, cc, dd, ee, ff, gg, hh, ii, jj, kk, ll, mm, nn, oo;

    for (int i = 0; i < (max - min); i++) {
        //Computes radiance at every wavelength interval for blackbody of given temprature
        SPD->wavelength[i] = min + (interval * i);

        aa = 2 * H;
        bb = pow(C, 2);
        cc = aa * bb;
        dd = pow((SPD->wavelength[i] / nm_to_m), 5);
        ee = cc / dd;

        ff = 1;
        gg = H * C;
        hh = SPD->wavelength[i] / nm_to_m;
        ii = K * temprature;
        jj = hh * ii;
        kk = gg / jj;
        ll = exp(kk);
        mm = ll - 1;
        nn = ff / mm;

        oo = ee * nn;

        SPD->radiance[i] = oo;
    }

    //for debug perposes
    printf("wavelength(nm) | radiance(Watts per steradian per meter squared)\n");
    for (int i = 0; i < (max - min); i++) {
        printf("%d %Le\n", SPD->wavelength[i], SPD->radiance[i]);
    }
    return 0;
}

Equation variable values during runtime in xcode:

variable values during runtime in xcode

Community
  • 1
  • 1
chewdonkey
  • 75
  • 9
  • 1
    Btw, why is `nm_to_m` equal to 1e-6 ? If it is nanometers to meters then shouldn't it be 1e-**9** ? – Yuriy Ivaskevych Mar 24 '17 at 17:04
  • You are absolutely right, a typo. This obviously have an effect on the size of the numbers, but unfortunately in the wrong direction, I have lots of 'inf' and 'nan' in the long double arrays now – chewdonkey Mar 24 '17 at 17:08
  • 1
    Unrelated to your problem, but I find it suspicious that you use ten significant figures for Plank's constant, and only four for the speed of light. – ad absurdum Mar 24 '17 at 17:21
  • 1
    IEEE floating point numbers only have 18 digits of precision. You know that, right? – duffymo Mar 24 '17 at 17:24
  • Thanks, that is the sort of thing I will need to start noticing if I do more work with equations in C – chewdonkey Mar 24 '17 at 17:27
  • The `exp` part tends to 1 which gives a division by zero error, hence the `Inf` for the radiance. `Inf/x` is `NaN` in most cases. – deamentiaemundi Mar 24 '17 at 18:05
  • Because you compute normalized value, it looks that you can simple drop e-34 from H constant and get more precise result. Probably you can omit e-23 from K. The main idea is to use values that will not overflow/underflow double during intermidiate calculations. Simple changing the order of computation might help with this. And about constants omited. You can multiple by them if you need absolute value. – alexander Mar 24 '17 at 21:01
  • The exponent range on `double` goes to ±308 or thereabouts, typically. It's unlikely that the small numbers are directly causing problems unless you're trying to add or subtract numbers of similar magnitude. It's quicker, simpler, and no less accurate to write `C * C` (or `(C * C)`) than it is to write `pow(C, 2)`. – Jonathan Leffler Mar 24 '17 at 21:52
  • You have `typedef struct { int *wavelength; long double *radiance; long double *normalised; } results;` — and you then allocate arrays for each of `wavelength`, `radiance` and `normalised`. You'd do better with `typedef struct { int wavelength; long double radiance; long double normalised; } results;` and then allocate an array of `results`: `results *SPD = malloc((max - min + 1) * sizeof(*SPD));` and then use `SPD[i].wavelength`, etc. (instead of `SPD->wavelength[i]`). – Jonathan Leffler Mar 24 '17 at 21:56
  • You appear to have an off-by-one error when you allocate `SPD->wavelength = malloc(sizeof(int) * (max - min));` and then use `for (int i = 0; i <= (max - min); i++) { SPD->wavelength[i] = min + (interval * i);` — your upper bound should be `< (max - min)` given the space you allocated, or you need to allocate `(max - min + 1)` units in the first place. – Jonathan Leffler Mar 24 '17 at 21:58
  • 1
    You said your graph is wrong, but it isn't obvious to me why it is wrong. Comparing it to [the one at Wikipedia](https://en.wikipedia.org/wiki/Planck%27s_law#/media/File:Black_body.svg) isn't trivial, even the units are different. Can you explain why you say it's wrong? Is it too high? Too low? The curve is too steep? You are computing for T = 200 K, but the graph at Wiki starts at T = 3000 K. Were you expecting something like the red line there (a sort of skewed gaussian) instead of the negative exponential? – Fabio says Reinstate Monica Mar 25 '17 at 02:35
  • @FabioTurati You are absolutely right - thank you so much. Changing the T to 5000K moved the blackbody curve into the spectral range I was plotting and so the results 'looked' how I was expecting them to. (Obviously I had also fixed a few errors in my equation before getting to this point). I will post the working code as an answer incase anyone else would like the same functionality in C. – chewdonkey Mar 25 '17 at 09:52

2 Answers2

4

I notice a couple of things that are wrong and/or suspicious about the current state of your program:

  • You have defined nm_to_m as 10-9,, yet you divide by it. If your wavelength is measured in nanometers, you should multiply it by 10-9 to get it in meters. To wit, if hh is supposed to be your wavelength in meters, it is on the order of several light-hours.
  • The same is obviously true for dd as well.
  • mm, being the exponential expression minus 1, is zero, which gives you infinity in the results deriving from it. This is apparently because you don't have enough digits in a double to represent the significant part of the exponential. Instead of using exp(...) - 1 here, try using the expm1() function instead, which implements a well-defined algorithm for calculating exponentials minus 1 without cancellation errors.
  • Since interval is 1, it doesn't currently matter, but you can probably see that your results wouldn't match the meaning of the code if you set interval to something else.
  • Unless you plan to change something about this in the future, there shouldn't be a need for this program to "save" the values of all calculations. You could just print them out as you run them.

On the other hand, you don't seem to be in any danger of underflow or overflow. The largest and smallest numbers you use don't seem to be a far way from 10±60, which is well within what ordinary doubles can deal with, let alone long doubles. The being said, it might not hurt to use more normalized units, but at the magnitudes you currently display, I wouldn't worry about it.

Dolda2000
  • 25,216
  • 4
  • 51
  • 92
  • Thanks a lot. It is really good to rule out the large/small numbers issue. I think I am right to be focusing on understanding the equation then. I have swapped to expm1() and changed nm_to_m to multiply, no luck yet though. Agreed - I might change interval if I want it to be an input at a later date – chewdonkey Mar 25 '17 at 09:16
3

Thanks for all the pointers in the comments. For anyone else running into a similar problem with implementing equations in C, I had a few silly errors in the code:

  • writing a 6 not a 9
  • dividing when I should be multiplying
  • an off by one error with the size of my array vs the iterations of for() loop
  • 200 when I meant 2000 in the temperature variable

As a result of the last one particularly I was not getting the results I expected (my wavelength range was not right for plotting the temperature I was calculating) and this was leading me to the assumption that something was wrong in the implementation of the equation, specifically I was thinking about big/small numbers in C because I did not understand them. This was not the case.

In summary, I should have made sure I knew exactly what my equation should be outputting for given test conditions before implementing it in code. I will work on getting more comfortable with maths, particularly algebra and dimensional analysis.

Below is the working code, implemented as a function, feel free to use it for anything but obviously no warranty of any kind etc.

blackbody.c

//
//  Computes radiance for every wavelength of blackbody of given temprature
//
//  INPUTS: int min wavelength to begin calculation from (nm), int max wavelength to end calculation at (nm), int temperature (kelvin)
//  OUTPUTS: pointer to structure containing:
//              - spectral radiance (Watts per steradian per meter squared per wavelength at 1nm intervals)
//              - normalised radiance
//

//include & define
#include "blackbody.h"

//global variables
const double H = 6.626070040e-34;   //Planck's constant (Joule-seconds) 6.626070040e-34
const double C = 299792458;         //Speed of light in vacuum (meters per second)
const double K = 1.3806488e-23;     //Boltzmann's constant (Joules per Kelvin) 1.3806488e-23
const double nm_to_m = 1e-9;        //conversion between nm and m
const int interval = 1;             //wavelength interval to calculate at (nm), to change this line 45 also need to be changed

bbresults* blackbody(int min, int max, double temperature) {
    double new_valu, old_valu = 0;      //variables for normalising result
    bbresults *SPD;
    SPD = malloc(sizeof(bbresults));
    //allocate memory based on wavelength bounds
    SPD->wavelength = malloc(sizeof(int) * (max - min));
    SPD->radiance = malloc(sizeof(long double) * (max - min));
    SPD->normalised = malloc(sizeof(long double) * (max - min));

    for (int i = 0; i < (max - min); i++) {
        //Computes radiance for every wavelength of blackbody of given temperature
        SPD->wavelength[i] = min + (interval * i);
        SPD->radiance[i] = ((2 * H * pow(C, 2)) / (pow((SPD->wavelength[i] * nm_to_m), 5))) * (1 / (expm1((H * C) / ((SPD->wavelength[i] * nm_to_m) * K * temperature))));

        //Copy SPD->radiance to SPD->normalised
        SPD->normalised[i] = SPD->radiance[i];

        //Find largest value
        if (i <= 0) {
            old_valu = SPD->normalised[0];
        } else if (i > 0){
            new_valu = SPD->normalised[i];
            if (new_valu > old_valu) {
                old_valu = new_valu;
            }
        }
    }

    for (int i = 0; i < (max - min); i++) {
        //Normalise SPD
        SPD->normalised[i] = SPD->normalised[i] / old_valu;
    }

    return SPD;
}

blackbody.h

#ifndef blackbody_h
#define blackbody_h

#include <stdio.h>
#include <math.h>
#include <stdlib.h>

//typedef structure to hold results
typedef struct {
    int *wavelength;
    long double *radiance;
    long double *normalised;
} bbresults;

//function declarations
bbresults* blackbody(int, int, double);

#endif /* blackbody_h */

main.c

#include <stdio.h>
#include "blackbody.h"

int main() {
    bbresults *TEST;
    int min = 100, max = 3000, temp = 5000;

    TEST = blackbody(min, max, temp);

    printf("wavelength | normalised radiance |           radiance               |\n");
    printf("   (nm)    |          -          | (W per meter squr per steradian) |\n");
    for (int i = 0; i < (max - min); i++) {
        printf("%4d %Lf %Le\n", TEST->wavelength[i], TEST->normalised[i], TEST->radiance[i]);
    }

    free(TEST);
    free(TEST->wavelength);
    free(TEST->radiance);
    free(TEST->normalised);
    return 0;
}

Plot of output:

plot of output

chewdonkey
  • 75
  • 9
  • 1
    It still looks wrong to me. Your curve for T = 5000 K, the blue one, is lower than the one for T = 4000 K, the orange one. According to Wikipedia, it should be the opposite. But the shape of the curves is really similar, maybe it's just that the legend is wrong and you have swapped 4000K and 5000K? – Fabio says Reinstate Monica Mar 25 '17 at 11:33
  • 1
    It should be noted that your current `blackbody()` function returns a pointer to a stack-allocated value, which is trouble. The fact that you're not segfaulting as it is, is probably just sheer luck. You should probably swap it to either return a `malloc`'d struct, or just return the struct as a value instead of a pointer to it. – Dolda2000 Mar 25 '17 at 17:15
  • @FabioTurati Thanks, you you are right, a mistake in excel, I have updated the image – chewdonkey Mar 26 '17 at 14:39
  • @Dolda2000 I thought I would not segfault because the struct and pointer were static. I am not sure I understand why returning the struct as a value is better than returning a static pointer to the struct in this case or why a malloc'd struct would be better than a static one? Reading around, I understand that it would be better to allocate memory space in the calling program to make blackbody() re-entry safe though. – chewdonkey Mar 26 '17 at 14:57
  • @chewdonkey: Oh, sorry, I missed the `static`. – Dolda2000 Mar 26 '17 at 15:08
  • You can find the maxima using the Wien's displacement constant, rather than looping through to find the largest value. – Chris_F Feb 19 '20 at 07:15