2

I've ported an approximation search algorithm from C++ to Python (the logic and very nice original implementation attributable to). I then wrote a script to use this algorithm in solving a 2-dimensional localization problem (the Time Difference of Arrival problem). The 2-dimensional solution worked great. When I nest to 3-dimensions, however, the script doesn't produce expected localizations.

Note that this almost certainly isn't an issue of the mathematics of solvability. In theory, the algorithm should be extendable to any n dimensions given n+1 receivers, so long as not all n+1 receivers lie in an n-1 dimensional space. In this case, I have 4 receivers for a 3 dimensional solution.

The approximation search algorithm can be found here. I've excluded it from this post as the issue almost certainly doesn't lie with this part of the code.

What I've tried:

I've tried stepping through this with pdb and a GUI debugger. I've also tried sprinkling in print statements to perform value checks. Unfortunately, due in part to the fact that there is so much going on in terms of calculations, I'm struggling to identify precisely where the issue occurs. I have a hunch it may have something to do with where I've placed ax = Appr... and ay = Appr..., however it's only a hunch (and, I've tried many combinations of placement with no success).

(Functioning) 2-Dimensional Solution:

def localize(recv):

    ax = Approximate(0, 5000, 32, 10)
    ay = Approximate(0, 5000, 32, 10)
    #az = Approximate(0, 5000, 32, 6)

    error = 0

    dt = [0, 0, 0]

    c = 299800000  # Speed of light
    baseline = 0


    while not ax.done:
        ay = Approximate(0, 5000, 32, 10)

        while not ay.done:

            for i in range(3):

                x = recv[i][0] - ax.a
                y = recv[i][1] - ay.a

                baseline = math.sqrt((x * x) + (y * y))

                dt[i] = baseline / c


            # Normalize times into deltas from zero
            baseline = min(dt[0], dt[1], dt[2])

            for j in range(3):
                dt[j] -= baseline

            error = 0.0
            error = 0.0

            for k in range(3):
                error += math.fabs(recv[k][2] - dt[k])
                ay.e = error; ax.e = error

            ay.step()

        ax.step()

    # Found solution
    print(ax.aa)
    print(ay.aa)

(Problematic) 3-Dimensional Solution:

def localize(recv):

    ax = Approximate(0, 5000, 32, 10)
    ay = Approximate(0, 5000, 32, 10)
    az = Approximate(0, 5000, 32, 10)

    error = 0

    dt = [0, 0, 0, 0]

    c = 299800000  # Speed of light
    baseline = 0


    while not ax.done:
        ay = Approximate(0, 5000, 32, 10)

        while not ay.done:
            az = Approximate(0, 5000, 32, 10)

            while not az.done:

                for i in range(4):

                    x = recv[i][0] - ax.a
                    y = recv[i][1] - ay.a
                    z = recv[i][2] - az.a

                    baseline = math.sqrt((x * x) + (y * y) + (z * z))

                    dt[i] = baseline / c


                # Normalize times into deltas from zero
                basline = min(dt[0], dt[1], dt[2], dt[3])

                for j in range(4):
                    dt[j] -= basline

                error = 0.0

                for k in range(4):
                    error += math.fabs(recv[k][3] - dt[k])
                    ay.e = error; ax.e = error; az.e = error

                az.step()

            ay.step()

        ax.step()

    # Found solution
    print(ax.aa)
    print(ay.aa)
    print(az.aa)


#Localization
# x = 1765   y = 2313   z = 1753
localize([[120,145,90,0.0000002468378075465656],
          [20,450,37,0.0000002433879002368936],
          [10,-50,324,0.0000002433879002368936],
          [67,903,45,0.0000002314851840328957]])

Predicted localization: 975.6857619200017, 811.0280894080021, 1278.482239584

Expected behavior: 1765, 2313, 753

(to within a fair degree of precision (C++ algorithm provides precision on the order of a fraction of a unit)).

Also, apologies for the messy code. It is in need of some streamlining.

EDIT:

As @jack pointed out below, the issue is almost certainly with my calculation of the error. I'm not sure what mistake I could possibly be making, however. It shouldn't be all that different from the 2-dimensional error calculation, it's a very basic minimization of summed squared errors problem. Not sure if it's a mathematical issue, or some coding issue that I've missed.

10GeV
  • 453
  • 2
  • 14
  • An observation: In the (working) 2d variant, you assign `min(dt)` to the misspelled `basname`, so that `basename` retains the value from inside the loop. – M Oehm Oct 25 '20 at 19:51
  • 1
    I think it would help to rewrite you algorithm in a way that it can be run on any number of dimensions. Right now you have two functions for the two cases, if you had one generic function instead this should naturally give the right result (or debugging would be easier at least). Apart from that, your two versions contain other differences such as in the 2D version you assign `c = 299800000 # Speed of light` however in the 3D version you don't. At least both versions should be identical for these kind of details, this will greatly help debugging too. – a_guest Oct 25 '20 at 19:54
  • @MOehm Sorry, not sure how that happened but `baseline` is spelled correctly in the functioning 2d variant. I've updated the post to reflect that (and re-run the code above to be sure). I've also updated the 3d variant to replicate as exactly as possible the 2d variant. Thanks for pointing that out, though! – 10GeV Oct 28 '20 at 22:33
  • Can you include Approximate class? – Kamil Nov 01 '20 at 04:21
  • @Kamil It can be found here: https://pastebin.com/yKs6mhcA – 10GeV Nov 01 '20 at 15:12
  • @KeithMadison does the current answer solve your purpose? – Abhinav Mathur Nov 02 '20 at 17:45
  • @AbhinavMathur While it is definitely a very thoughtful answer, unfortunately it did not add to what I already know. The issue is almost certainly with the computation of the error, and not with the `Approximate` class (as @jack pointed out). The problem is, I don't know what that issue could be? It isn't obvious to me that I've made a mistake, but I *must* have. So, I guess the question is: what mistake have I made in calculating the error? Maybe I've made a mathematical mistake, or maybe a coding mistake? Unfortunately, no one has been answered that yet. – 10GeV Nov 02 '20 at 17:57
  • @KeithMadison okay, I'll look into this then – Abhinav Mathur Nov 02 '20 at 19:32
  • @AbhinavMathur Thanks! Much appreciated. – 10GeV Nov 02 '20 at 20:46
  • @KeithMadison Hi I am back (have resolved my network problems)... Have tried your setup and found the problem and remedy... see my answer in here – Spektre Nov 06 '20 at 19:04

2 Answers2

1

Well I ported my original 2D solution into 3D and then tried your values... Looks like I found the problem.

In 3D and 3 receivers there are many positions with the same TDoA values hence the first found is returned (which might not be the one you seek). To verify just print the simulated TDoA values for found solution and compare to your input TDoA values. Another option is to print the final optimization error variable for outer most loop (in your case ax.e0) after computation and see if it is near zero. If it is it means approximation search found solution...

To remedy your case you need at least 4 receivers... so just add one ... Here my updated 3D C++/VCL code:

//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#pragma hdrstop

#include "Unit1.h"
#include "backbuffer.h"
#include "approx.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
backbuffer scr;
//---------------------------------------------------------------------------
// TDoA Time Difference of Arrival
// https://stackoverflow.com/a/64318443/2521214
//---------------------------------------------------------------------------
const int n=4;          // number of receivers
double recv[n][4];      // (x,y,z) [m] receiver position,[s] time of arrival of signal
double pos0[3];         // (x,y,z) [m] object's real position
double pos [3];         // (x,y,z) [m] object's estimated position
double v=299800000.0;   // [m/s] speed of signal (light)
double err=0.0;         // [m] error
double aps_e=0.0;       // [m] aprox search error
bool _recompute=true;
//---------------------------------------------------------------------------
void compute()
    {
    int i;
    double x,y,z,a,da,t0;
    //---------------------------------------------------------
    // init positions
    da=2.0*M_PI/(n);
    for (a=0.0,i=0;i<n;i++,a+=da)
        {
        recv[i][0]=2500.0+(2200.0*cos(a));
        recv[i][1]=2500.0+(2200.0*sin(a));
        recv[i][2]=500.0*sin(a);
        }
    // simulate measurement
    t0=123.5;                           // some start time
    for (i=0;i<n;i++)
        {
        x=recv[i][0]-pos0[0];
        y=recv[i][1]-pos0[1];
        z=recv[i][2]-pos0[2];
        a=sqrt((x*x)+(y*y)+(z*z));      // distance to receiver
        recv[i][3]=t0+(a/v);            // start time + time of travel
        }
    //---------------------------------------------------------
    // normalize times into deltas from zero
    a=recv[0][3]; for (i=1;i<n;i++) if (a>recv[i][3]) a=recv[i][3];
    for (i=0;i<n;i++) recv[i][3]-=a;
    // fit position
    int N=8;
    approx ax,ay,az;
    double e,dt[n];
              // min,   max,  step,recursions,&error
     for (ax.init( 0.0,5000.0,200.0,         N,   &e);!ax.done;ax.step())
      for (ay.init( 0.0,5000.0,200.0,         N,   &e);!ay.done;ay.step())
       for (az.init( 0.0,5000.0, 50.0,         N,   &e);!az.done;az.step())
        {
        // simulate measurement -> dt[]
        for (i=0;i<n;i++)
            {
            x=recv[i][0]-ax.a;
            y=recv[i][1]-ay.a;
            z=recv[i][2]-az.a;
            a=sqrt((x*x)+(y*y)+(z*z));  // distance to receiver
            dt[i]=a/v;                  // time of travel
            }
        // normalize times dt[] into deltas from zero
        a=dt[0]; for (i=1;i<n;i++) if (a>dt[i]) a=dt[i];
        for (i=0;i<n;i++) dt[i]-=a;
        // error
        e=0.0; for (i=0;i<n;i++) e+=fabs(recv[i][3]-dt[i]);
        }
    pos[0]=ax.aa;
    pos[1]=ay.aa;
    pos[2]=az.aa;
    aps_e=ax.e0;    // approximation error variable e for best solution
    //---------------------------------------------------------
    // compute error
    x=pos[0]-pos0[0];
    y=pos[1]-pos0[1];
    z=pos[2]-pos0[2];
    err=sqrt((x*x)+(y*y)+(z*z));        // [m]
    }
//---------------------------------------------------------------------------
void draw()
    {
    scr.cls(clBlack);
    int i;
    const double pan_x=0.0;
    const double pan_y=0.0;
    const double zoom=512.0/5000.0;
    double x,y,r=8.0;

    #define tabs(x,y){ x-=pan_x; x*=zoom; y-=pan_y; y*=zoom; }
    #define trel(x,y){ x*=zoom; y*=zoom; }

    scr.bmp->Canvas->Font->Color=clYellow;
    scr.bmp->Canvas->TextOutA(5, 5,AnsiString().sprintf("Error: %.3lf m",err));
    scr.bmp->Canvas->TextOutA(5,25,AnsiString().sprintf("Aprox error: %.20lf s",aps_e));
    scr.bmp->Canvas->TextOutA(5,45,AnsiString().sprintf("pos0 %6.1lf %6.1lf %6.1lf m",pos0[0],pos0[1],pos0[2]));
    scr.bmp->Canvas->TextOutA(5,65,AnsiString().sprintf("pos  %6.1lf %6.1lf %6.1lf m",pos [0],pos [1],pos [2]));

    // recv
    scr.bmp->Canvas->Pen->Color=clAqua;
    scr.bmp->Canvas->Brush->Color=clBlue;
    for (i=0;i<n;i++)
        {
        x=recv[i][0];
        y=recv[i][1];
        tabs(x,y);
        scr.bmp->Canvas->Ellipse(x-r,y-r,x+r,y+r);
        }
    // pos0
    scr.bmp->Canvas->Pen->Color=TColor(0x00202060);
    scr.bmp->Canvas->Brush->Color=TColor(0x00101040);
    x=pos0[0];
    y=pos0[1];
    tabs(x,y);
    scr.bmp->Canvas->Ellipse(x-r,y-r,x+r,y+r);

    // pos
    scr.bmp->Canvas->Pen->Color=clYellow;
    x=pos[0];
    y=pos[1];
    tabs(x,y);
    scr.bmp->Canvas->MoveTo(x-r,y-r);
    scr.bmp->Canvas->LineTo(x+r,y+r);
    scr.bmp->Canvas->MoveTo(x+r,y-r);
    scr.bmp->Canvas->LineTo(x-r,y+r);

    scr.rfs();

    #undef tabs(x,y){ x-=pan_x; x*=zoom; y-=pan_y; y*=zoom; }
    #undef trel(x,y){ x*=zoom; y*=zoom; }
    }
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
    {
    Randomize();
    pos0[0]=2000.0;
    pos0[1]=2000.0;
    pos0[2]= 900.0;
    scr.set(this);
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
    {
    draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::tim_updateTimer(TObject *Sender)
    {
    if (_recompute)
        {
        compute();
        _recompute=false;
        }
    draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormClick(TObject *Sender)
    {
    pos0[0]=scr.mx1*5000.0/512.0;
    pos0[1]=scr.my1*5000.0/512.0;
    pos0[2]=Random()*1000.0;
    _recompute=true;
    }
//---------------------------------------------------------------------------

So as before just ignore the VCL stuff and port to your environment/language... Its configured so it does not compute for too long (less than 1 sec) and the error is <0.01 m

Here preview:

preview

also with printed the approximation error variable final value (abs sum of difference between solution's and input TDoA times in seconds) ... and pos0,pos values...

Beware even this is not fully safe as it has blind spots ... The receivers should not be at the same altitude and also maybe even not distributed evenly on circle because that can cause the TDoA duplicates...

In case you got additional info (like know that pos0 is at ground and have the terrain map) then it might be possible to do this with 3 receivers where the z coordinate will not be approximated anymore but computed from x,y instead...

PS. your nesting has a slight cosmetics "bug" as you have:

          ay.e = error; ax.e = error; az.e = error
          az.step()

        ay.step()

    ax.step()

I would feel much safer with (not sure though as I do not code in Python):

          az.e = error
          az.step()

        ay.e = error
        ay.step()

    ax.e = error
    ax.step()
Spektre
  • 49,595
  • 11
  • 110
  • 380
0

The issue is not with how the code performs the search. Taking out where the error is calculated are replacing with a simple test case gives (very close to) the expected answer

def testError( x , y , z ):
    tp = [435.0 , 733.0 ,1771.0]
    return (x - tp[0])**2 + (y - tp[1])**2 + (z - tp[2])**2 + ( x*y*z - tp[0]*tp[1]*tp[2] )**2

def localize(recv):
    ax = Approximate(0, 5000, 32, 10)
    while not ax.done:
        ay = Approximate(0, 5000, 32, 10)
        while not ay.done:
            az = Approximate(0, 5000, 32, 10)
            while not az.done:
                error = testError( ax.a , ay.a , az.a )
                ay.e = error; ax.e = error; az.e = error
                az.step()
            ay.step()
        ax.step()
    # Found solution
    print(ax.aa)
    print(ay.aa)
    print(az.aa)

Running just where the code calculates the error, for the example the expected behaviour point gives an error of 3.38e-06 and the point the search found gives 2.31e-07. So the issue is in how the error is calculated , a possible problem is that the times in dt have thier min taken from them, but this is not done for the times in recv. However this is the same in the 2d case, and changing this gives a different unexpected point (570.96,614.52,922.02) which again has a lower calculated error than the expected behaviour point.

jack
  • 61
  • 1