7

I have 3 receivers (A, B and C), and some signal producing source (let's say sound or light) with an unknown location. Given the locations of A,B and C, and the time at which each receiver "heard" the signal, I'd like to determine the direction of the source.

I understand there are ways to do so with TDoA multilateration/trilateration, however I'm having trouble implementing the calculation. There isn't a lot of clear, detailed information on this out there for those entirely new to the subject. What is out there is vague, more theoretical, or a bit too esoteric for me.

Some similar posts on SO (but not quite what I'm after): TDOA multilateration to locate a sound source Trilateration of a signal using Time Difference(TDOA)

This is also interesting, but assumes we have some boundaries: Multiliteration implementation with inaccurate distance data

@Dave also commented an excellent and fairly accessible resource https://sites.tufts.edu/eeseniordesignhandbook/files/2017/05/FireBrick_OKeefe_F1.pdf, but it falls short of going into enough depth that one might be able to actually implement this in code (at least, for someone without deep knowledge of regression, finding the intersection of the resulting hyperbolas, etc).

[EDIT]: I should add that I can assume the 3 sensors and the source are on the surface of the Earth, and the effects of the curvature of the Earth are negligible (i.e. we can work in 2-dimensions).

10GeV
  • 453
  • 2
  • 14
  • 1
    What is _TDoA_, and what does it have to do with c++? – πάντα ῥεῖ Sep 02 '20 at 14:29
  • Time-Delay of Arrival (there are a few other posts on here about TDoA multilateration, but none quite cover what I need). I'll be implementing this in C++, so I think that tag is relevant. – 10GeV Sep 02 '20 at 14:31
  • No, the tag is relevant for asking about concrete problems with c++. Better remove it from your question, mention the links you've found, and state that you preferrably look for an implementation in c++. – πάντα ῥεῖ Sep 02 '20 at 14:35
  • 1
    For me, it is *Time Difference of Arrival*. Compared to *ToA*, this avoids knowing at which time exactly the signal was sent. @πάντα ῥεῖ – Damien Sep 02 '20 at 14:36
  • @Damien yes, this is what I'm referring to. – 10GeV Sep 02 '20 at 14:39
  • You need a fourth sensor to guarantee a unique solution. E.g., if nothing else, for all solutions not in the plane defined by the three sensors, you won't be able to tell which side of the plane the signal is on. – Dave Sep 02 '20 at 16:37
  • 1
    @Dave Oh, yes, I should've mentioned this. I'm assuming here that the sensors and the source are all on the surface of the earth (i.e. we can work in 2D here), and also that the effects of the curvature of the earth are negligible. – 10GeV Sep 02 '20 at 16:59
  • https://sites.tufts.edu/eeseniordesignhandbook/files/2017/05/FireBrick_OKeefe_F1.pdf << Tufts student project on this; isn't too technical. – Dave Sep 02 '20 at 17:27
  • How your problem is different from the second [post you linked](https://stackoverflow.com/questions/36176167/trilateration-of-a-signal-using-time-differencetdoa)? – user58697 Sep 02 '20 at 18:11
  • Just write the equations of the form "time of arrival is a function of unknown coordinates of the source" and solve for the coordinates. You need to know the coordinates of each receiver of course. – n. m. could be an AI Sep 02 '20 at 18:31
  • @user3386109 The receivers are approximately 1 km apart, and the distance to the source is unknown. – 10GeV Sep 12 '20 at 22:51

2 Answers2

2

Interesting problem. I am too lazy to derive equations for algebraic solution. Instead why not fit the result?

So simply fit the 2D (or higher) position using any fitting method capable of finding local solution (using optimization/minimization of some error value). When I use mine simple approximation search to fit the position the results looks pretty good.

The algorithm is:

  1. iterate through "all" positions on your range

    of coarse not all the heuristics of the fitting will reduce the problem a lot.

  2. on each tested position compute the delta times that would be measured

    simple time of travel from tested position into your receiver stations.

  3. normalize all delta times so the start from zero

    so substract the smallest time of arrival from all the recivers times. The same do for the real measured times. This assures the times does not involve relative offset.

  4. compute the difference between real measured times and computed ones

    simple abs difference is enough. Use this value as fitting parameter (optimization).

Here small C++ example doing this using my approx class from link above:

//---------------------------------------------------------------------------
// TDoA Time Difference of Arrival
//---------------------------------------------------------------------------
const int n=3;
double recv[n][3];  // (x,y) [m] receiver position,[s] time of arrival of signal
double pos0[2];     // (x,y) [m] object's real position
double pos [2];     // (x,y) [m] object's estimated position
double v=340.0;     // [m/s] speed of signal
double err=0.0;     // [m] error between real and estimated position
//---------------------------------------------------------------------------
void compute()
    {
    int i;
    double x,y,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]=256.0+(220.0*cos(a));
        recv[i][1]=256.0+(220.0*sin(a));
        }
    pos0[0]=300.0;
    pos0[1]=220.0;
    // 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];
        a=sqrt((x*x)+(y*y));    // distance to receiver
        recv[i][2]=t0+(a/v);    // start time + time of travel
        }
    //---------------------------------------------------------
    // normalize times into deltas from zero
    a=recv[0][2]; for (i=1;i<n;i++) if (a>recv[i][2]) a=recv[i][2];
    for (i=0;i<n;i++) recv[i][2]-=a;
    // fit position
    int N=6;
    approx ax,ay;
    double e,dt[n];
              // min,  max,step,recursions,&error
    for (ax.init( 0.0,512.0, 32.0        ,N,   &e);!ax.done;ax.step())
     for (ay.init(  0.0,512.0, 32.0       ,N,   &e);!ay.done;ay.step())
        {
        // simulate measurement -> dt[]
        for (i=0;i<n;i++)
            {
            x=recv[i][0]-ax.a;
            y=recv[i][1]-ay.a;
            a=sqrt((x*x)+(y*y));    // 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][2]-dt[i]);
        }
    pos[0]=ax.aa;
    pos[1]=ay.aa;
    //---------------------------------------------------------
    // compute error
    x=pos[0]-pos0[0];
    y=pos[1]-pos0[1];
    err=sqrt((x*x)+(y*y));  // [m]
    }
//---------------------------------------------------------------------------

Here preview:

preview

Blue dots are the receivers, Red dot is real position of object and yellow cross is its estimated position. The area is 512x512 m and I fit it with initial step 32 m and 6 recursions leading to error ~0.005 m

I am more than happy with the result... You can change the number of receivers n without any real change to source or algorithm. I initiated the receiver positions as uniformly distributed on circle but the positions might be any other (not all on single line of coarse)

Spektre
  • 49,595
  • 11
  • 110
  • 380
  • Thanks for this (very!) well thought out answer. I'd actually tried precisely this, but my algorithm *always* produced an answer at approximately that "x" in your image (my receivers are similarly placed), no matter what data I fed the algorithm (including data designed specifically to produce a localization outside the circle formed by the three observers). With the live data, I know the source to be far away from the three observers, yet the localization still produced this point. I'll have a look at this. Perhaps I made some stupid mistake. Thanks again! – 10GeV Oct 12 '20 at 16:43
  • @KeithMadison my bet is you use wrong fitting method like binary search or CCD which tends to miss solutions for non monotonic functions. Or wrongly configured fitting its important that initial step of the fitting do not miss local min/max. I just tested `pos0 ` outside the receivers `(20,420)` and it fits even better with error `0.004 m` with the same settings. Btw I developed that approximation search for cases exactly like this (where different methods fail) – Spektre Oct 13 '20 at 05:29
  • Well, it seems you're right! Thanks again, this helped significantly. One final question, in your example above you set `min = 0.0` and `max = 512.0`. Would the algorithm then check in a 512x512 area centered on the point `[0,0]`, or would it check the area from 0-512 along both axis (hopefully that makes as much sense here as in my head). I suspect the former, but I wanted to be sure. – 10GeV Oct 13 '20 at 18:23
  • @KeithMadison `min` is low and `max` is high boundary so it checks square with diagonal `(0,0) , (512,512)` +/- two initial steps (it can cross slightly during recursions) so center is`( 256,256)` Of coarse you can use any coordinates and even different ones for each axis. You can look at it as a `for` cycle from `min` to `max` with step `step/(10^N)` but instead of brute force it uses heuristics so its much faster. The actual iterated value is in `??.a` and the best solution is in `??.aa` where `??` is the name of the approx class. If you want more info see the linked QA where the source is – Spektre Oct 13 '20 at 19:10
  • @KeithMadison Btw. I used those coordinates as they fit my app window (512x512 pixels) top left corner is `(0,0)` and bottom right is `(511,511)` so I do not need any zoom or transformations for view to keep the example as simple as I could. – Spektre Oct 13 '20 at 19:20
  • Apologies for the bother. I've written a `Python` implementation of an approximation search algorithm, effectively identical to your own. I used it to implement this 2D solution, to great success. When I implement a 3D solution, however, it does not localize properly (https://stackoverflow.com/questions/64527856/problem-nesting-approximation-search-algorithm). I'm wondering if I've erred in the nesting? I could be a code issue as opposed to a logic issue (in which case this need be it's own question), but I don't think it is. – 10GeV Oct 29 '20 at 20:42
  • @KeithMadison will need to try it in C++ with mine with your data to check if the stuff just do not miss solution due wrong settings of step ... however I have no internet service at home right now so i will try it once that is resolved (may be next week) – Spektre Nov 03 '20 at 09:59
0

The simplest (but not fastest) approach would be to solve the equations with gradient descent.

I'm assuming that we know

  • the positions of the receivers, A, B, and C, which do not lie on the same line;
  • the pseudorange of the unknown source X to each of A, B, and C.

Intuitively, we simulate a physical system with three ideal springs configured like so, where the equilibrium length of each spring is the corresponding pseudorange.

  A
  |
  X
 / \
B   C

The springs push when the distance is too small and pull when it's too large. The approximate resting place of X should be a reasonable estimate (though depending on your application you might want to do additional validation).

Here's some sample Python code using complex numbers as 2D vectors that should be easy to transliterate.

import random


def distance(p, q):
    return abs(p - q)


# Force exerted by an ideal spring between variable y and fixed q of equilibrium
# length dxq.
def force(y, q, dxq):
    dyq = distance(y, q)
    return (dxq - dyq) * (y - q) / dyq


# Trilateration via gradient descent.
def trilaterate(
    a, dxa, b, dxb, c, dxc, *, max_iterations=1000000, gamma=0.001, precision=1e-12
):
    # Use the centroid of the receivers as the initial estimate.
    y = (a + b + c) / 3
    for i in range(max_iterations):
        f = force(y, a, dxa) + force(y, b, dxb) + force(y, c, dxc)
        y += gamma * f
        if abs(f) <= precision:
            return y
    return None


def random_point():
    return complex(random.random(), random.random())


def test_error():
    a = random_point()
    b = random_point()
    c = random_point()
    x = random_point()
    y = trilaterate(a, distance(x, a), b, distance(x, b), c, distance(x, c))
    return distance(x, y)


if __name__ == "__main__":
    print(test_error())
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120