SWHarden.com

The personal website of Scott W Harden

Streaming Digits of Pi

Exploring a strategy to calculate infinite digits of π without floating point arithmetic

This page describes a strategy for calculating pi one digit at a time using a streaming algorithm that does not require floating point arithmetic. Although there are more exotic strategies for calculating pi, this one is surprisingly simple to implement, and it streams the digits of pi infinitely. Before we dive in, give it a try in your browser!

Theory

It has long been known that pi can be represented as the product of an infinite series. The Wallis Product published in 1656 represents Pi as:

Which can be expanded to:

And alternatively expressed as:

The paper Spigot Algorithm for the Digits of Pi (1995) by Stanley Rabinowitz and Stan Wagon describes how this infinite product series can be calculated in base 10 to stream the output digit by digit. The algorithm is is described in the paper on page 5. All mathematical operations are achieved using integer arithmetic, completely avoiding issues associated with floating point error accumulation. It is worth noting that the integers are large, so overflows with typical integer data types will occur quickly. It is necessary to use arbitrary length integer data types (BigInt) or to store large integers in an array pre-sized according to the number of digits to be calculated (a little over 3 times the number of digits) which is O(N) space complexity. Because the algorithm requires iterating the array fully for every digit to be calculated, this strategy runs in O(N2) time.

Appendix 2 has a useful bit of code attributed to “Macalester student Simeon Simeonov” implementing the algorithm in Pascal. It notes that this code makes use of the fact that the queue of predigits always has a pile of 9s to the right of its leftmost member, and so only this leftmost predigit and the number of 9s need be remembered.

Pascal Implementation

Program PiSpigot;

const
  n   = 1000;
  len = 10 * n div 3;

var
  i, j, k, q, x, nines, predigit: integer;
  a: array[1..len] of longint;

begin
  for j := 1 to len do
    a[j] := 2;  {Start with 2s}

  nines := 0;
  predigit := 0;  {First predigit is a 0}

  for j := 1 to n do
  begin
    q := 0;

    for i := len downto 1 do  {Work backwards}
    begin
      x := 10 * a[i] + q * i;
      a[i] := x mod (2 * i - 1);
      q := x div (2 * i - 1);
    end;

    a[1] := q mod 10;
    q := q div 10;

    if q = 9 then
      nines := nines + 1
    else if q = 10 then
    begin
      write(predigit + 1);

      for k := 1 to nines do
        write(0);  {zeros}

      predigit := 0;
      nines := 0;
    end
    else
    begin
      write(predigit);
      predigit := q;

      if nines <> 0 then
      begin
        for k := 1 to nines do
          write(9);

        nines := 0;
      end;
    end;
  end;

  writeln(predigit);
end.

C# Implementation

A line-by-line translation to C# is as follows

int maxDigits = 1000;
int len = (10 * maxDigits) / 3;

int[] a = new int[len + 1];
int nines = 0;
int predigit = 0;

for (int j = 1; j <= len; j++)
    a[j] = 2;

for (int j = 1; j <= maxDigits; j++)
{
    int q = 0;

    for (int i = len; i >= 1; i--)
    {
        int x = 10 * a[i] + q * i;
        a[i] = x % (2 * i - 1);
        q = x / (2 * i - 1);
    }

    a[1] = q % 10;
    q /= 10;

    if (q == 9)
    {
        nines++;
    }
    else if (q == 10)
    {
        Console.Write(predigit + 1);

        for (int k = 1; k <= nines; k++)
            Console.Write(0);

        predigit = 0;
        nines = 0;
    }
    else
    {
        if (j > 1)
            Console.Write(predigit);
        if (j == 2)
            Console.Write(".");

        predigit = q;

        if (nines != 0)
        {
            for (int k = 1; k <= nines; k++)
                Console.Write(9);

            nines = 0;
        }
    }
}

It generates the first thousand digits of pi in about 60 milliseconds. The first 10,000 took 3.3 sec, and additional performance benchmarks can be found at the bottom of this page. The output is accurate according to some online tables I found of pi to a large number of decimal places.

3.14159265358979323846264338327950288419716939937510582097494459230781640628620899862803482534211706798214808651328230664709384460955058223172535940812848111745028410270193852110555964462294895493038196442881097566593344612847564823378678316527120190914564856692346034861045432664821339360726024914127372458700660631558817488152092096282925409171536436789259036001133053054882046652138414695194151160943305727036575959195309218611738193261179310511854807446237996274956735188575272489122793818301194912983367336244065664308602139494639522473719070217986094370277053921717629317675238467481846766940513200056812714526356082778577134275778960917363717872146844090122495343014654958537105079227968925892354201995611212902196086403441815981362977477130996051870721134999999837297804995105973173281609631859502445945534690830264252230825334468503526193118817101000313783875288658753320838142061717766914730359825349042875546873115956286388235378759375195778185778053217122680661300192787661119590921642019

Calculate Pi in your Browser

I implemented the Spigot Algorithm for the Digits of Pi in JavaScript and adapted it to run in the browser. It utilizes JavaScript’s BigInt type to manage math with arbitrarily large integers and runs with a performance comparable to the C# example above.

Performance of the JavaScript implementation is surprisingly good. You can generate virtually infinite digits of pi using your desktop or mobile device. You never know when you’re going to need a few thousand digits of pi, and it is always good to be prepared.

Performance

This a bak of the envelope test just using my moderate Desktop PC and the C# implementation above, but performance is pretty good for several thousand digits and balloons after that as expected for an algorithm with O(N2) time complexity.

Digits of Pi Calculation Time Approximate Memory
1,000 62 ms 13 kB
10,000 3.31 sec 133 kB
100,000 4.93 min 1.3 MB
1,000,000 8.12 hr 13 MB

Resources


WiFi Temperature Sensor with NodeMCU

An internet-enabled digital thermometer for temperature logging and live time series visualization

I spent the last few years experimenting with designs for internet-enabled temperature sensors so I could monitor the relationship between my indoor, outdoor, garage, and attic temperatures over long time spans. This page documents my favorite implementations using NodeMCU, a USB powered and programmable Arduino-compatible IoT development board with an ESP8266 WiFi module that can be purchased on Amazon in small quantities for about $5 each. The design presented on this page uses a DS18B20 1-Wire thermometer packaged in a weatherproof enclosure sold in small quantities for about $2 each.

Connections

Connecting the 1-Wire sensor only requires adding an external resistor to pull the data pin high. I found it convenient to solder the resistor and sensor wires directly to pins of the NodeMCU development board. It is worth noting that the DS18B20 is available in several different form factors, but the communication protocol and wiring requirements are the same.

Embedded Software

The NodeMCU can be programmed via its micro USB connection using the Arduino IDE. Full source code for this project is available on GitHub, but the following sketch is a trimmed-down version which is likely sufficient for others to replicate this project.

#include <ESP8266WiFi.h>
#include <ESP8266HTTPClient.h>
#include <OneWire.h>
#include <DallasTemperature.h>

OneWire oneWire(D2); // Data wire connected to D2
DallasTemperature sensors(&oneWire);

void setup() {
  sensors.begin();
  WiFi.begin("WIFI_SSID", "WIFI_PASSWORD"); // Customize for your network
  while (WiFi.status() != WL_CONNECTED) {delay(1000);}
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);
}

void loop() {
  // Read temperature
  sensors.requestTemperatures();
  float temp = sensors.getTempFByIndex(0);

  // Prepare the endpoint URL
  String resource = String("https://swharden.com/weather/v1/write/");

  // Create JSON body for the POST request
  String data = String("{")
                + String("\"key\": \"MY_SECRET_API_KEY\",") // Customize for security
                + String("\"sensor\": \"3\",") // Sensor ID for multi-sensor environments
                + String("\"temperature\": \"") + String(temp) + String("\",")
                + String("}");

  // Create a WiFi client that does not use SSL
  WiFiClientSecure client;
  client.setInsecure();
  client.connect("swharden.com", 443);

  // Make the POST request including our custom JSON body
  HTTPClient http;
  http.begin(client, resource);
  http.addHeader("Content-Type", "application/json");
  http.POST(data);
  http.end();

  // Wait one minute before retrying
  int seconds = 60;
  while (seconds--) {delay(1000);}
}

Temperature Dashboard

Users will want to customize their web server architecture, but I achieved a live temperature dashboard using a PHP endpoint that interprets the body of the POST request and logs the time/sensor/temperature as a single line appended to a CSV file. The interactive graphs are achieved using Dygraph, a JavaScript library ideal for interactively displaying time-series data on webpages.

Indoor WiFi Module with Outdoor Temperature Sensor

I am fortunate to already have easy access to an indoor/outdoor pass-through created to run HF and VHF coax cabling from indoor radios to outdoor antennas. This made it easy to house the weather-sensitive NodeMCU module inside while snaking the weather-insensitive temperature sensor outside. This is on the North side of my house too, so it’s always in shadow and it should be a pretty good location for direct air temperature measurement.

Garage Temperature Sensor

The sensor in my garage is perhaps the cleanest build. I kept the USB cable short and only had the tip of the temperature sensor stick out the top. The NodeMCU has blue LEDs on it already, so I drilled a hole in the enclosure (sealed off with clear plastic glued to the underside of the hole) allowing the blinking light to be observed from outside. Short flashes indicate it is waiting until the next measurement, and it turns solid while it’s measuring temperature and submitting the HTTP request.

Weatherproof Design for Outdoor Use

Originally I created a “weatherproof” enclosure to house the NodeMCU microcontroller and temperature sensor, powered using low voltage delivered through landscaping wire typically used for lard lighting. This design used a BMP280 sensor (described below) to measure temperature and barometric pressure. I put “weatherproof” in quotes because after almost two years it failed. The experience was an interesting (and humbling) venture into designing IoT gear intended to stand up against the elements. For what it’s worth, it did make it through two Florida hurricanes.

Alternative Designs to Measure Temperature and Pressure with BMP280

Previous WiFi temperature sensor designs used the BMP280 I2C temperature and pressure sensor which I had on hand from previous precision pressure measurement project, but the software required to read temperature and pressure was surprisingly complex due to the calibration procedure described in the datasheet. In contrast, the design using the SD18B20 digital temperature sensor was extremely easy to implement. There are Arduino sketches on this project’s GitHub repository demonstrating how to use both sensors with NodeMCU.

Additional Resources


Resampling Segmented Lines for Evenly Spaced Points

How to create a collection of points along a segmented line evenly spaced in line distance or 2D space

I recently had the need to convert a segmented line (polyline) with arbitrary node points to one with evenly spaced points. After exploring a few solutions I settled on the following two strategies because of their simplicity and performance. Code on this page avoids trigonometric functions and square root operations to enhance performance.

Resampling for Equal Distance Along the Length of the Line

Simple linear interpolation may be employed to place points at specific distances along the length of the line. This method minimizes computations but it may place points closer together in 2D space than the specified distance, a phenomenon which is exacerbated when the original polyline contains very sharp angles.

// Start with a segmented line with arbitrary points
List<Vector2> polyline1 = [new(1, 1), new(3, 8), new(5, 2), new(9, 9)];

// Generate a new segmented line with evenly spaced points
double spacing = 1;
List<Vector2> polyline2 = ResampleAlongLine(polyline1, spacing);
List<Vector2> ResampleAlongLine(List<Vector2> polyline1, float spacing)
{
    List<Vector2> polyline2 = [];
    
    // start at the same first point as the original line
    polyline2.Add(new(polyline1[0].X, polyline1[0].Y));

    float distanceToNextPoint2 = spacing;
    for (int i = 1; i < polyline1.Count; i++)
    {
        Vector2 p0 = polyline1[i - 1];
        Vector2 p1 = polyline1[i];

        float dx = p0.X - p1.X;
        float dy = p0.Y - p1.Y;
        float distanceToNextPoint1 = (float)Math.Sqrt(dx * dx + dy * dy);

        while (distanceToNextPoint2 <= distanceToNextPoint1)
        {
            float t = distanceToNextPoint2 / distanceToNextPoint1;
            float x = p0.X + t * (p1.X - p0.X);
            float y = p0.Y + t * (p1.Y - p0.Y);
            polyline2.Add(new Vector2(x, y));

            // consider the point we just placed p0 and re-evaluate
            p0 = polyline2.Last();
            distanceToNextPoint1 -= distanceToNextPoint2;
            distanceToNextPoint2 = spacing;
        }

        // move backwards as needed before advancing to the next given pair
        distanceToNextPoint2 -= distanceToNextPoint1;
    }

    return polyline2;
}

Resampling for Equal Euclidean Distance in 2D Space

This implementation walks along interpolated line segments in short steps and places new points as the target distance from the previous point measured in 2D space has been met. This strategy can be significantly slower than the linear interpolation only method described above, but this implementation ensures that sequential points are always the target distance apart in 2D space (equal Euclidean spacing on a Cartesian coordinate system) and may be preferred for some applications. The accuracy can be increased by reducing the epsilon at the expense of increased computing time. Performance may be improved by starting with a large epsilon and decreasing it dynamically as the calculated distance between the test point and previous point approaches the target separation.

// Start with a segmented line with arbitrary points
List<Vector2> polyline1 = [new(1, 1), new(3, 8), new(5, 2), new(9, 9)];

// Generate a new segmented line with evenly spaced points
double spacing = 1;
List<Vector2> polyline2 = ResampleIn2D(polyline1, spacing);
List<Vector2> ResampleIn2D(List<Vector2> polyline, float spacing, float epsilon = 0.0001f)
{
    List<Vector2> polyline2 = [];

    // start at the same first point as the original line
    polyline2.Add(new(polyline[0].X, polyline[0].Y));

    // compare this because calculating square roots is very costly
    float spacingSquared = spacing * spacing;

    // consider each line segment between pairs of points in the given polyline
    for (int i = 1; i < polyline.Count; i++)
    {
        Vector2 p1 = polyline[i - 1];
        Vector2 p2 = polyline[i];

        // use epsilon to determine how many points along the line to try
        float sdx = p1.X - p2.X;
        float sdy = p1.X - p2.X;
        float segLength = (float)Math.Sqrt(sdx * sdx + sdy * sdy);
        int stepCount = (int)(segLength / epsilon);

        // check the distance at each step along the segment
        for (int j = 0; j <= stepCount; j++)
        {
            // decide the test point using linear interpolation
            float fraction = (float)j / stepCount;
            float lx = p1.X + (p2.X - p1.X) * fraction;
            float ly = p1.Y + (p2.Y - p1.Y) * fraction;
            Vector2 pTest = new(lx, ly);

            // add the test point if it's the right distance from the last one
            float dx = polyline2.Last().X - pTest.X;
            float dy = polyline2.Last().Y - pTest.Y;
            float pTestDistanceSquared = (dx * dx + dy * dy);
            if (pTestDistanceSquared >= spacingSquared)
            {
                polyline2.Add(pTest);
            }
        }
    }

    return polyline2;
}

Full Source Code

Below is the single file .NET 9 Console Application I used to generate the figures on this page. ScottPlot.net was used to generate the images. Note that Vector2 has a Lerp() method for linear interpolation and Distance() and DistanceSquared() methods for measuring distances between points, but I chose to implement these functions discretely to make the code on this page easier to translate to other languages.

View the full source code
using System.Numerics;

List<Vector2> polyline1 = [new(1, 1), new(3, 8), new(5, 2), new(9, 9)];

for (int i = 1; i <= 10; i++)
{
    List<Vector2> polyline2 = ResampleAlongLine(polyline1, i);
    Plotting.Plot(polyline1, polyline2, i, "line", "Even Spacing Along Polyline", ScottPlot.Colors.Black.WithAlpha(.5));

    List<Vector2> polyline3 = ResampleIn2D(polyline1, i);
    Plotting.Plot(polyline1, polyline3, i, "2d", "Even Spacing in 2D Space", ScottPlot.Colors.Magenta.WithAlpha(.5));
    ShowDistances(i, polyline3);
}

static void ShowDistances(float expected, List<Vector2> polyline)
{
    var distances = Enumerable.Range(0, polyline.Count - 1)
        .Select(i => Vector2.Distance(polyline[i], polyline[i + 1]));

    Console.WriteLine($"[{expected}] " + string.Join(", ", distances));
}

static List<Vector2> ResampleAlongLine(List<Vector2> polyline1, float spacing)
{
    if (polyline1.Count < 2)
        throw new InvalidDataException("need at least 2 points");

    if (spacing <= 0)
        throw new InvalidDataException("spacing must be positive");

    List<Vector2> polyline2 = []; // will be filled with evenly-spaced points
    polyline2.Add(new(polyline1[0].X, polyline1[0].Y)); // start at the same first point

    float distanceToNextPoint2 = spacing;
    for (int i = 1; i < polyline1.Count; i++)
    {
        Vector2 p0 = polyline1[i - 1];
        Vector2 p1 = polyline1[i];

        float dx = p0.X - p1.X;
        float dy = p0.Y - p1.Y;
        float distanceToNextPoint1 = (float)Math.Sqrt(dx * dx + dy * dy);

        while (distanceToNextPoint2 <= distanceToNextPoint1)
        {
            float t = distanceToNextPoint2 / distanceToNextPoint1;
            float x = p0.X + t * (p1.X - p0.X);
            float y = p0.Y + t * (p1.Y - p0.Y);
            polyline2.Add(new Vector2(x, y));

            // consider the point we just placed p0 and re-evaluate
            p0 = polyline2.Last();
            distanceToNextPoint1 -= distanceToNextPoint2;
            distanceToNextPoint2 = spacing;
        }

        // move backwards as needed before advancing to the next given pair
        distanceToNextPoint2 -= distanceToNextPoint1;
    }

    return polyline2;
}

static List<Vector2> ResampleIn2D(List<Vector2> polyline, float spacing, float epsilon = 0.0001f)
{
    List<Vector2> polyline2 = [];

    // start at the same first point as the original line
    polyline2.Add(new(polyline[0].X, polyline[0].Y));

    // compare this because calculating square roots is very costly
    float spacingSquared = spacing * spacing;

    // consider each pair of points in the given polyline
    for (int i = 1; i < polyline.Count; i++)
    {
        Vector2 p1 = polyline[i - 1];
        Vector2 p2 = polyline[i];

        // use epsilon to determine how many points along the line to try
        int stepCount = (int)(Vector2.Distance(p1, p2) / epsilon);
        for (int j = 0; j <= stepCount; j++)
        {
            // decide the test point using linear interpolation
            float fraction = (float)j / stepCount;
            float lx = p1.X + (p2.X - p1.X) * fraction;
            float ly = p1.Y + (p2.Y - p1.Y) * fraction;
            Vector2 pTest = new(lx, ly);

            // add the test point if it's the right distance from the last one
            float dx = polyline2.Last().X - pTest.X;
            float dy = polyline2.Last().Y - pTest.Y;
            float pTestDistanceSquared = (dx * dx + dy * dy);
            if (pTestDistanceSquared >= spacingSquared)
            {
                polyline2.Add(pTest);
            }
        }
    }

    return polyline2;
}

public static class Extensions
{
    public static ScottPlot.Coordinates[] ToCoordinates(this List<Vector2> points)
    {
        return [.. points.Select(x => new ScottPlot.Coordinates(x.X, x.Y))];
    }
}

static class Plotting
{
    public static void Plot(List<Vector2> points1, List<Vector2> points2, int spacing, string name, string legend, ScottPlot.Color color)
    {
        ScottPlot.Plot plot = new();
        var sp1 = plot.Add.Scatter(points1.ToCoordinates());
        sp1.Color = ScottPlot.Colors.C1;
        sp1.LineWidth = 2;
        sp1.MarkerSize = 7;
        sp1.LegendText = "Original Polyline";

        var markers = plot.Add.Markers(points2.ToCoordinates());
        markers.Color = color;
        markers.MarkerShape = ScottPlot.MarkerShape.OpenCircle;
        markers.MarkerSize = 13;
        markers.MarkerLineWidth = 2;
        markers.LegendText = legend;

        plot.Axes.SetLimits(0, 10, 0, 10);
        plot.SavePng($"spacing-{name}-{spacing}.png", 700, 400);
    }
}

Additional Resources


Performant Data Smoothing in C#

Exploring the Halving Bidirectional Simple Moving Average (HBSMA) strategy for generating progressively smoothed curves from noisy datasets

Optimized Simple Moving Average Algorithm

The simple moving average (SMA) for an array of length N using a window of size W can be efficiently calculated using a running sum maintained in memory. As the window moves along the data, new values are shifted into it by adding their value to the running sum, and old values are shifted out by subtracting their value from the running sum.

This strategy reduces the number of operations from approximately O(N*W) to O(N) which offers considerable performance enhancement for applications requiring large smoothing windows.

double[] SmoothForward(double[] data, int windowSize)
{
    double[] smooth = new double[data.Length];
    double runningSum = 0;
    int pointsInSum = 0;
    for (int i = 0; i < smooth.Length; i++)
    {
        runningSum += data[i];
        if (pointsInSum < windowSize)
        {
            pointsInSum++;
            smooth[i] += runningSum / pointsInSum;
            continue;
        }
        runningSum -= data[i - windowSize];
        smooth[i] += runningSum / windowSize;
    }
    return smooth;
}

Optimized Bidirectional Simple Moving Average Algorithm

Applying two successive moving window averages in opposite directions is a quick way to achieve bidirectional smoothing and prevents the “trailing” effect caused by a lagging running average which can be seen in the example above.

Bidirectional smoothing doubles the number of operations from O(N) to O(N*2) but this strategy used here is still far more performant than O(N*W) realized by convolution with a triangular kernel which is expected to produce a similar result.

double[] SmoothBidirectional(double[] data, int windowSize)
{
    double[] smooth = new double[data.Length];

    // smooth from left to right
    double runningSum = 0;
    int pointsInSum = 0;
    for (int i = 0; i < smooth.Length; i++)
    {
        runningSum += data[i];
        if (pointsInSum < windowSize)
        {
            pointsInSum++;
            smooth[i] += runningSum / pointsInSum;
            continue;
        }
        runningSum -= data[i - windowSize];
        smooth[i] += runningSum / windowSize;
    }

    // smooth from right to left
    runningSum = 0;
    pointsInSum = 0;
    for (int i = smooth.Length - 1; i >= 0; i--)
    {
        runningSum += data[i];
        if (pointsInSum < windowSize)
        {
            pointsInSum++;
            smooth[i] += runningSum / pointsInSum;
            continue;
        }
        runningSum -= data[i + windowSize];
        smooth[i] += runningSum / windowSize;
    }

    // average the two directions
    for (int i = 0; i < smooth.Length; i++)
        smooth[i] /= 2;

    return smooth;
}

Halving Bidirectional Simple Moving Average (HBSMA)

Successively smoothing data using repeatedly halved window sizes is a strategy I have recently found to be extremely useful for aggressively smoothing large amounts of signal data over long timescales. This HBSMA strategy is especially good at smoothing discontinuous data with “jumps” in the signal. With a performance approximating O(N*log(W)*2) it offers vastly superior performance over convolution with a Gaussian kernel, while producing roughly similar output.

I think of this strategy has having three primary functions that each act according to the size of the window:

A looping strategy is demonstrated here, but a recursive algorithm could also be realized to achieve the same effect.

public static double[] SmoothBidirectionalHalving(double[] data, int windowSize)
{
    double[] smooth = data;
    while (windowSize > 0)
    {
        smooth = SmoothBidirectional(smooth, windowSize);
        windowSize /= 2;
    }
    return smooth;
}

Faster than Gaussian Convolution with Similar Results

Let’s compare results of the halving bidirectional simple moving average (HBSMA) with a traditional convolution strategy using a Gaussian kernel. In this strategy our rectangular window is replaced with a bell-shaped window (a kernel) which is used to produce a weighted moving average for each point in the original dataset. A Gaussian window provides bidirectional smoothing, but since it never reaches zero on the edges I prefer using a Hanning window for applications like this.

The HBSMA strategy produces a result strikingly similar with the Gaussian convolution, but in less than one tenth of the time. Performance benchmarking on my system revealed a 10x improvement in speed for the data above, but demonstrated over 130x performance improvement for larger signals (10,000 points) smoothed with a bigger window (100 points).

Moving Window Padding Strategies

It is worth noting that moving window averaging strategies must account for the situation at the ends of the original data where there are not enough data points to fill the window. In these cases a “padding” strategy may be used to fill the window to ensure the same number of data points are averaged to create each value on the smoothed curve. The examples in this article do not use fancy padding (their behavior is best described by the “None” option below), but it is worth being aware of common padding strategies:

Conclusion

The Halving Bidirectional Simple Moving Average (HBSMA) strategy enables data smoothing with results similar to Gaussian convolution but in a small fraction of the time. The performance of HBSMA is approximately O(N*log(M)*2) whereas the convolution strategy is approximately O(N*M), and the larger the smoothing window size is the more gains may be had by this data smoothing strategy.

Additional Resources


Curve Fitting in C# using Particle Swarm Optimization

How to fit arbitrary equations to X/Y data points using C# and the SwarmFit package

This article describes how to fit arbitrary curves with any number of parameters to X/Y data points using C#. The strategy described here uses particle swarm optimization to iteratively work toward an optimal solution. Unlike other curve fitting strategies, the first derivative of the error function does not need to be calculated mathematically, making it particularly useful for problems where the solution landscape is noisy, nonlinear, or completely unknown. Users only need to provide a function and the limits for each parameter, and the swarm will converge on the optimal solution.

SwarmFit is a free and open-source NuGet package that makes it easy to fit arbitrary curves to data. I created SwarmFit after researching the topic of particle swarm optimization in preparation for this article. Although this page details how particle swarm optimization works under the hood, SwarmFit be used without any knowledge of the topic.

Quickstart

// data points to fit
double[] xs = [1, 2, 3, 4, 5];
double[] ys = [304, 229, 174, 134, 111];

// a custom function to fit to the data using any number of parameters
static double MyFitFunc(double x, double[] parameters)
{
	double a = parameters[0];
	double b = parameters[1];
	double c = parameters[2];
	return a + b * Math.Exp(x * c);
}

// the minimum and maximum value for each parameter
double[] minVars = [-100, -5000, -10];
double[] maxVars = [100, 5000, 10];

// perform the fit with general purpose settings
double[] solution = QuickFit.Solve(xs, ys, MyFitFunc, minVars, maxVars);

// display the solution
double a = solution[0];
double b = solution[1];
double c = solution[2];
Console.WriteLine($"Y = {a} + {b} * e^(x * {c})");

Advanced users may wish to use F12 to peek inside the QuickFit.Solve() method to see how it creates a SwarmFitter, initializes it, and runs the solver. The SwarmFitter has many properties which may be adjusted to change the behavior of the fitter for enhanced logging, improved performance, or potentially better fits. See the SwarmFit GitHub repository for additional information.

Theory

Particle swarm optimization is a type of swarm intelligence algorithm that represents candidate solutions as particles in a swarm that all work together to minimize the error function. In this strategy, a set of parameter values is represented by a particle, and a small swarm of particles iteratively works toward a solution by each of their parameters to minimize the total difference between the data points and the curve generated by those parameters.

Particles move toward the optimal solution by combining their own values and recent experience with information gathered from neighboring particles. The combination of self-guided and socially guided behavior allows the swarm to explore a broad range of solutions while progressively converging onto the best one. Randomness is added to the system by randomly killing particles and spawning new ones, increasing the chances that particles will find the global minimum and not get trapped in a local one.

Implementation

Here’s a summary of how particle swarm optimization may be implemented in C#. Full source code may be reviewed from the SwarmFit GitHub repository and additional information is available in the resources section at the bottom of the page.

Preparation

Optimization Steps

Resources


Simple Exponential Fit with C#

How to fit an exponential curve to data points using pure csharp

This article describes how to perform a simple exponential fit using pure C#. Exponential curves which pass through 0 can be described using two variables, so data can be translated into linear space, fitted using a linear least squares strategy, then translated back into exponential space.

// These are the data points we wish to fit
double[] xs = [1, 2, 3, 4, 5, 6, 7];
double[] ys = [258, 183, 127, 89, 65, 48, 35];

// Log-transform values, then perform a linear fit
double[] logYs = ys.Select(x => Math.Log(x)).ToArray();
(double slope, double intercept) = LeastSquaresFit(xs, logYs);

// Convert back to exponential form and display the result
double a = Math.Exp(intercept);
Console.WriteLine($"y = {a}*e^({slope}*x)");

Here’s the Linear Least Squares implementation used by the code sample above:

static (double slope, double intercept) LeastSquaresFit(double[] xs, double[] ys)
{
    double sumX = 0, sumY = 0, sumX2 = 0, sumXY = 0;

    for (int i = 0; i < xs.Length; i++)
    {
        sumX += xs[i];
        sumY += ys[i];
        sumX2 += xs[i] * xs[i];
        sumXY += xs[i] * ys[i];
    }

    double avgX = sumX / xs.Length;
    double avgY = sumY / xs.Length;
    double avgX2 = sumX2 / xs.Length;
    double avgXY = sumXY / xs.Length;
    double slope = (avgXY - avgX * avgY) / (avgX2 - avgX * avgX);
    double intercept = (avgX2 * avgY - avgXY * avgX) / (avgX2 - avgX * avgX);
    return (slope, intercept);
}

Plotting Data

The graphs above were made using ScottPlot.NET using the following code:

ScottPlot.Plot plot = new();

// plot the original data and the fitted curve
var dataMarkers = plot.Add.ScatterPoints(xs, ys);
dataMarkers.MarkerSize = 10;
dataMarkers.LegendText = "Raw Data";

// generate and plot the fitted curve
double[] fitXs = Enumerable.Range(0, 100).Select(x => x * .1).ToArray();
double[] fitYs = fitXs.Select(x => a * Math.Exp(slope * x)).ToArray();
var fitLine = plot.Add.ScatterLine(fitXs, fitYs);
fitLine.LineWidth = 2;
fitLine.LinePattern = ScottPlot.LinePattern.DenselyDashed;
fitLine.LegendText = "Fitted Curve";

// decorate the plot and save it
plot.Legend.Alignment = ScottPlot.Alignment.UpperRight;
plot.Title($"y = {a:N2}*e^({slope:N2}*x)");
plot.SavePng("fit.png", 400, 300);

Optimizing Curves for Other Formulas

The code above is ideal for calculating two terms to generate a fitted curve according to the formula:

Y = A * eB * x

…but what about fitting to a curve with third term describing a Y offset?

Y = C + A * eB * x

…or a forth term describing an X offset too?

Y = C + A * eB * x + D

The linear least squares fitting strategy described above is not sufficient for fitting data to these complex curves. The linear least squares strategy can be applied in higher dimensional space using gradient descent to seek optimized curves for more advanced equations. This strategy is summarized by The Math Coffeeshop in a fantastic YouTube video about using Linear Least Squares to Solve Nonlinear Problems . However, formulas representing the partial derivative of each unknown in the error function must be calculated which may be difficult. An alternative strategy is to use Particle swarm optimization to iteratively work toward an optimal solution for a fitted curve even if the derivative of the error function cannot be calculated. This strategy is described in my follow-up article, Curve Fitting in C# using Particle Swarm Optimization

Resources


GitHub License Checker

A tool for checking the license key for all of your GitHub repositories

Adding a permissive license to your open source projects makes it easy for others to benefit from reusing your code. I typically recommend the MIT license for most projects because it’s simple, well understood, and allows the code to be used for essentially any purpose. Using code from websites and GitHub repositories without a license is difficult, because as I understand it the default terms are “all rights reserved” by the copyright holder unless explicitly stated otherwise. Since not having a license puts restrictions on how others may use your code, I created the GitHub License Checker tool to help identify GitHub repositories with missing license files.

Launch: https://swharden.com/github-license-checker/

It’s a Vanilla JavaScript app that works by hitting the paginated “repos” GitHub API for a given username. Because a page has up to 100 repositories and most users are unlikely to have more than a few hundred repositories, the API rate limiting is rarely engaged so a GitHub API key is not required.

https://api.github.com/users/<USER>/repos?per_page=100&page=1

The GitHub repositories API returns a lot of useful information. Because it’s so easy to use client-side without an API key, I’m curious what other useful tools could be created using it. Perhaps ScottPlot could be used to generate some interesting charts. Ideas that jump out are plotting every repo’s star count vs size, or date vs number of commits. I’ll leave that for another day, but C# developers should note that the Octokit NuGet Package is an official .NET package for interacting with the GitHub API.

Resources