Metadata-Version: 2.5
Name: findakin
Version: 0.2.6
Summary: Population-level questions on collections of astronomical spectra: similarity search, clustering, anomalies, trajectories and encoder benchmarks on a local LanceDB store, with embeddings from spectro-kernel.
Project-URL: Homepage, https://findakin.lelain.net
Project-URL: Repository, https://github.com/matthieulel/findakin
Project-URL: Documentation, https://findakin.lelain.net/docs/
Project-URL: Changelog, https://github.com/matthieulel/findakin/blob/main/CHANGELOG.md
Author: matthieulel
License: MIT
License-File: LICENSE
Keywords: anomaly-detection,astronomy,clustering,embeddings,lancedb,mcp,similarity-search,spectra,spectroscopy
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.11
Requires-Dist: astropy>=6.0
Requires-Dist: httpx>=0.27
Requires-Dist: lancedb>=0.20
Requires-Dist: numpy>=1.26
Requires-Dist: pandas>=2.0
Requires-Dist: pyarrow>=15
Requires-Dist: pydantic>=2.6
Requires-Dist: scikit-learn>=1.4
Requires-Dist: scipy>=1.12
Requires-Dist: spectro-kernel>=0.8
Requires-Dist: typer>=0.12
Provides-Extra: all
Requires-Dist: fastmcp>=2.0; extra == 'all'
Requires-Dist: hdbscan>=0.8.38; extra == 'all'
Requires-Dist: kaleido>=1.0; extra == 'all'
Requires-Dist: minisom>=2.3; extra == 'all'
Requires-Dist: plotly>=5.20; extra == 'all'
Requires-Dist: pyvo>=1.5; extra == 'all'
Requires-Dist: umap-learn>=0.5.6; extra == 'all'
Provides-Extra: analysis
Requires-Dist: hdbscan>=0.8.38; extra == 'analysis'
Requires-Dist: minisom>=2.3; extra == 'analysis'
Requires-Dist: umap-learn>=0.5.6; extra == 'analysis'
Provides-Extra: dev
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest>=8.3; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.5; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
Requires-Dist: zensical>=0.0.62; extra == 'docs'
Provides-Extra: encoders
Requires-Dist: torch>=2.2; extra == 'encoders'
Provides-Extra: mcp
Requires-Dist: fastmcp>=2.0; extra == 'mcp'
Requires-Dist: kaleido>=1.0; extra == 'mcp'
Requires-Dist: plotly>=5.20; extra == 'mcp'
Provides-Extra: viz
Requires-Dist: plotly>=5.20; extra == 'viz'
Provides-Extra: vo
Requires-Dist: pyvo>=1.5; extra == 'vo'
Description-Content-Type: text/markdown

# Findakin

**Find a kin for every spectrum.**

Findakin answers *population* questions about collections of astronomical spectra:
which look alike, what structure they form, which stand out, which changed in time, and
which representation (encoder) is the most physical. It is a local Python library built on
[spectro-kernel](https://github.com/matthieulel/spectro-kernel) (one spectrum → one vector)
and [LanceDB](https://lancedb.com) (an embedded, versioned, columnar store with a vector
index). No server, no account: your spectra stay on your disk.

> **Status: 0.2 alpha, API may still change.** Findakin started as a hosted web application
> (FastAPI + MongoDB + Qdrant + React, retired in October 2026) and was rebuilt as a
> local-first library. Landing page: [findakin.lelain.net](https://findakin.lelain.net) ·
> Documentation: [findakin.lelain.net/docs](https://findakin.lelain.net/docs/).

```
spectro-kernel  = algorithms on ONE spectrum   (embeddings, I/O, lines, RV, reduction…)
Findakin        = questions on MANY spectra    (index, neighbours, clusters, anomalies,
                                                trajectories, encoder benchmarks)
```

![406 pro-am spectra of three stars read back as spectra: mean and spread per star on a common grid](https://raw.githubusercontent.com/matthieulel/findakin/main/docs/images/campaign/stars-as-spectra.png)

*Three stars of a pro-am campaign ([STAROS](https://staros-projects.org)) read back as spectra after ingestion, embedding and grouping. The documentation works through this population and through 566 SDSS stars, with provenance.*

## Install

```bash
pip install findakin                 # core: store, ingestion, kernel recipes, k-NN, analysis toolbox
pip install "findakin[viz]"          # + the Plotly views (maps, overlays, cluster means, trajectories…)
pip install "findakin[analysis]"     # + UMAP, HDBSCAN, self-organising maps (minisom)
pip install "findakin[vo]"           # + SSAP / Virtual Observatory connectors
pip install "findakin[encoders]"     # + local PyTorch encoders (spectro-kernel embed_pretrained)
pip install "findakin[mcp]"          # + findakin-mcp: the store served to Claude or any MCP client, locally
```

Python ≥ 3.11.

## Quick start

```python
from findakin import Store

st = Store.open("~/findakin/store")                 # a directory; created if missing

# 1. ingest — FITS (BeSS / ISIS / specINTI / Demetra dialects), CSV, JSON, TXT.
#    Spectra are deduplicated by content; metadata (object, time, instrument,
#    observer, site, resolution, coverage) is extracted from the headers.
report = st.ingest_folder("~/spectra/staros", source="staros", tags=["campaign:staros"])
print(report)                                       # 406 files → 406 added, 0 duplicates, 0 errors
print(st.resolve_objects())                         # 31 spellings → 3 SIMBAD identifiers (main_id), 6 left to you
st.set_object("adraconis", "alf Dra")               # a manual alias; then the series follow the identifiers
st.set_series()
st.measure("snr_der")                               # the kernel measures each spectrum; findakin keeps the values

# 2. embed — any spectro-kernel strategy; several encoders coexist for the same spectra
enc = st.embed(strategy="dct", dim=256, norm_method="z_score")   # → 'dct:v1:256:nogrid:z_score'
enc_ll = st.embed(strategy="log_lambda", dim=256)                 # Doppler-robust recipe
# tip: with min_max the continuum term dominates the vector and every pair scores ≈ 0.99;
# z_score (zero mean) or continuum_subtracted let the lines drive the similarity.

#    a named common grid, stored once: every spectrum resampled on 6545–6580 Å, the sharper
#    ones degraded to R = 8000 at constant R; encoders and flux matrices read the same rows
st.define_grid("halpha", 6545, 6580, resolution=8000)
st.apply_grid("halpha")                                           # 399 added; 345 degraded, 35 coarser, 19 R unknown
enc_ha = st.embed(strategy="dct", dim=64, norm_method="z_score", grid="halpha")   # → 'dct:v1:64:halpha:z_score'
st.create_index(enc)                                              # a vector index, for 10^5+ vectors

# 3. ask
st.neighbours("~/spectra/new_alfdra.fits", k=10, encoder=enc)          # k-NN by file…
st.neighbours(some_id, k=10, encoder=enc, where="resolution > 15000")   # …or by id, filtered
ids, X = st.vectors(enc, where="main_id = 'alf Dra'")                   # numpy matrix for analysis
pop = st.define_population("alfdra-2023", "main_id = 'alf Dra' AND t_mjd BETWEEN 59945 AND 60310")
ids, X = pop.vectors(enc)                                                # the same, with a name that results keep
st.near_duplicates(enc)                                                  # the same observation stored twice?

from findakin.analysis import cluster_embeddings, cluster_stability, detect_anomalies, decompose, series_trajectory
res = cluster_embeddings(X, method="agglomerative", n_clusters=2)     # Ward; linkage kept for a dendrogram
cluster_stability(X, method="agglomerative", n_clusters=2)           # is the partition reproducible?
weird = detect_anomalies(X, method="urf", contamination=0.05)        # unsupervised random forest weirdness
stack = st.flux_matrix(grid="halpha", where="series_id = 'alf Dra'") # the spectra on the grid
pca = decompose(stack.flux, "pca", n_components=5)                   # eigenspectra + reconstruction error
ids_s, t, XS = st.series("alf Dra", enc)                              # one series in time order
tr = series_trajectory(XS, t, ids=ids_s, period=51.42)               # outliers, change points, orbital phase

# 4. look — every view is a Plotly figure (pip install "findakin[viz]")
from findakin import viz
viz.cluster_spectra(stack.wavelength, stack.flux, res.labels)        # clusters read back as spectra
viz.leakage_view(project_embeddings(X).points, meta, by=("object", "instrument"))
viz.trajectory_plot(tr, x="phase", y=pca.weights[:, 0])             # the orbit, folded
```

Every method is glue over scikit-learn, scipy, astropy, minisom or plotly, cites its
reference in the docstring, and returns a result aligned with the input ids.

Same verbs on the command line:

```bash
findakin ingest ~/spectra/alphadra --source staros --tag campaign:alphadra
findakin resolve --series
findakin measure snr_der
findakin embed --strategy dct --dim 256
findakin grid define halpha --wl-min 6545 --wl-max 6580 --resolution 8000 && findakin grid apply halpha
findakin embed --strategy dct --dim 64 --norm-method z_score --grid halpha
findakin index dct:v1:256:nogrid:min_max
findakin search ~/spectra/new_alfdra.fits -k 10 -e dct:v1:64:halpha:z_score
findakin population define alfdra "main_id = 'alf Dra'"
findakin notebook campaign --data ~/spectra/alphadra        # a notebook to start from
claude mcp add findakin -- findakin-mcp --store ~/findakin/store   # then ask Claude about your populations
findakin info
```

## What is in the store

A LanceDB directory, versioned, rebuildable from your files:

| Table | Content |
|---|---|
| `spectra` | one row per spectrum: `id` (content hash), `source`, `object`, `main_id` (resolved), `series_id`, `t_mjd`, `instrument`, `spectrograph`, `observer`, `site`, `resolution`, `wl_min/wl_max`, `file_uri`, `tags`, raw header (JSON) |
| `objects` / `populations` | every spelling met in the headers with its SIMBAD identifier; the named SQL filters with their size and version |
| `flux_native` | the native wavelength / flux arrays (re-embed with a new encoder without re-reading files) |
| `grids` / `flux__<grid>` | the named common grids, and each spectrum on each grid: flux, coverage, native resolution, `degraded` flag |
| `emb__<encoder>` | one table per encoder: `(id, vector, provenance)` — `encoder_id = strategy:model_version:dim:grid:norm` — with an optional vector index |
| `encoders` | the encoder registry (config, dimension) |
| `results` | analyses and kernel measurements stored with their provenance: name, kind, encoder, population, params, `{id: value}` payload, store version |

`series_id` groups spectra that form a trajectory in time: an object followed over epochs
(default), a night of sky spectra (`series_from="night"`, e.g. aurorae), or a folder.

## Roadmap (short)

Phase 0 — store, ingestion, kernel encoders, k-NN, CLI ✅ · Phase 2a — population toolbox
(clusters + stability, URF anomalies, eigenspectra / NMF, trajectories, SOM), stored results,
Plotly views ✅ · Phase 1 — named common λ grids in the store (`flux__<grid>`, resolution
matching at constant R), vector indexes ✅ · Phase 2b — named populations, SIMBAD aliases,
notebook templates ✅ · Phase 4 — `findakin-mcp` over the store, locally ✅ · Phase 3 —
learned encoders registry, benchmark with an *instrument-leakage* score · Phase 5 — thin
HTTP façade for applications.

## Development

```bash
uv venv .venv --python 3.12 && source .venv/bin/activate
uv pip install -e ".[dev]"
pytest -q
ruff check src tests
```

The landing page lives in [website/](website/) (Vite + React, deployed separately).

## License

MIT — © 2026 Matthieu Le Lain.
