# My 0x20

**Category**: Astronomy, Astrophysics, Astrometry, Astrodynamics, AAAA
**Points (final)**: 142 points
**Solves**: 24

> This social media app is back with a vengeance!

**Given files**: `myspace-mike33686zulu.tar.bz2`

## Write-up
by [erin (`barzamin`)](https://imer.in).

0x20 is SpaceBook, except without a backdoor. If you haven't read that writeup yet, go take a look at it.

That is, the trick in SpaceBook -- magnitudes are trivially matchable -- is gone. Additionally, we have less superbright outliers in the provided unknown star set. We can no longer match specific stars trivially. However, we can still solve the problem: this is what _real_ star trackers have to do (although they have to deal with stuff like measurement and catalog error that don't show up here).

Since the observed (unknown) stars are all one simple rigid transformation (the observation orientation!) away from their positions in the catalog, relative distances and angles are preserved. Assuming that the local neighborhood of each star is represented relatively well in the observation set, we can look at the neighborhood of each star -- hopefully -- to determine what star it is.

Vaguely inspired by _Pattern Recognition of Star Constellations for Spacecraft Applications_, C. C. Liebe 1993 (thanks, google), I designed a simple star fingerprint: every star is represented by a 3-vector containing:

- the distance to the closest neighbor
- the distance to the second-closest neighbor
- the angle between the two closest neighbors, relative to the star under consideration.

To make finding nearest neighbors fast, I used scikit-learn's [ball tree implementation](https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.BallTree.html). I could then easily generate fingerprints for any given set of stars and its ball tree (warning: CTF-quality ML code from here on, sorry):

```{.python}
def angle(x, y):
    x_ = x/np.linalg.norm(x)
    y_ = y/np.linalg.norm(y)
    return np.arccos(np.clip(np.dot(x_, y_), -1.0, 1.0))

def gen_fingerprints(X, bt):
    dist, ind = bt.query(X[:], k=3)
    fingerprints = []
    for i in range(X.shape[0]):
        a = X[i,:]
        [bi, ci] = ind[i,1:]
        b = X[bi,:]
        c = X[ci,:]
        fingerprints.append([
            *dist[i, 1:],
            angle(b-a, c-a),
        ])
    return fingerprints
```

We could then fingerprint the star catalog:
```{.python}
with open('./myspace-mike33686zulu/test.txt') as f:
    catalog = read_starfile(f.read())

X = np.vstack([s['v'] for s in catalog])
bt_catalog = BallTree(X, leaf_size=30)

fingerprints = gen_fingerprints(X, bt_catalog)
```

You could do something probably more noise resistant, like finding $N$ best-match fingerprints, doing Kabsch on those pairs, and then finding the rest by L2 norm similarity between boresight vectors after rotating to the catalog orientation. I just spat out the index of the closest catalog fingerprint match for each unknown star, throwing out fingerprint matches with error that was too high (L2 distance between fingerprints > $1\times10^{-4}$). Do that for every challenge, and you're done:
```{.python}
for _ in range(5):
    refstars = read_starfile(r.recvuntil('\n\n').decode())

    Y = np.vstack([s['v'] for s in refstars])
    bt_unknown = BallTree(Y, leaf_size=30)

    fingerprints_unknown = gen_fingerprints(Y, bt_unknown)

    matches = []
    for query in fingerprints_unknown:
        error = [np.linalg.norm(np.array(fp_known) - np.array(query))
                 for fp_known in fingerprints]
        match_idx = np.argmin(error)
        if error[match_idx] < 1e-4:
            print(match_idx, error[match_idx])
            matches.append(match_idx)

    r.send(','.join(map(str,matches)) + '\n')
    r.recvuntil('Left...\n')
print(r.clean())
```

Fun fact! We got second or third on this (I don't remember), even though I had the code done in _literally this state_ before anyone else submitted a solution. This was entirely my fault, since I was *accidentally using the SpaceBook star catalog*.

### Full code
```{.python include=zoxtwenty.py}
```

## Resources and other writeups
- <https://ieeexplore.ieee.org/document/180383>
- <https://scikit-learn.org/stable/modules/generated/sklearn.neighbors.BallTree.html>
