Metadata-Version: 2.4
Name: erotica
Version: 0.2.0
Summary: EROTICA: Estimation, Recovery & Optimization, together with Inference, for Cluster Analysis
Author-email: Lucas Pulgar-Escobar <lescobar2019@udec.cl>
License-Expression: AGPL-3.0-or-later
Project-URL: Homepage, https://github.com/notluquis/erotica
Project-URL: Repository, https://github.com/notluquis/erotica
Project-URL: Issues, https://github.com/notluquis/erotica/issues
Project-URL: Changelog, https://github.com/notluquis/erotica/blob/main/CHANGELOG.md
Keywords: astronomy,clustering,gaia,star-clusters,machine-learning
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Scientific/Engineering :: Astronomy
Requires-Python: >=3.13
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: hdbscan>=0.8.44
Requires-Dist: optuna>=4.9
Requires-Dist: numpy>=2.4
Requires-Dist: scipy>=1.18
Requires-Dist: pandas>=3.0
Requires-Dist: astropy>=8
Requires-Dist: scikit-learn>=1.9
Requires-Dist: tqdm>=4.65
Requires-Dist: matplotlib>=3.11
Requires-Dist: dill>=0.3.8
Requires-Dist: adjusttext>=1.4
Requires-Dist: gaiadr3-zeropoint>=0.1.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.14; extra == "dev"
Requires-Dist: mypy>=1.5; extra == "dev"
Requires-Dist: pre-commit>=3.0; extra == "dev"
Requires-Dist: nbstripout>=0.7; extra == "dev"
Provides-Extra: oracles
Requires-Dist: ocelot>=0.4.10; extra == "oracles"
Provides-Extra: docs
Requires-Dist: sphinx>=7.0; extra == "docs"
Requires-Dist: pydata-sphinx-theme>=0.15; extra == "docs"
Requires-Dist: numpydoc>=1.6; extra == "docs"
Requires-Dist: myst-parser>=2.0; extra == "docs"
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
Requires-Dist: sphinx-design>=0.6; extra == "docs"
Provides-Extra: examples
Requires-Dist: jupyter>=1.0; extra == "examples"
Requires-Dist: jupyterlab>=4.0; extra == "examples"
Requires-Dist: ipywidgets>=8.0; extra == "examples"
Requires-Dist: seaborn>=0.12; extra == "examples"
Provides-Extra: bayes
Requires-Dist: pytensor; extra == "bayes"
Requires-Dist: pymc>=6; extra == "bayes"
Requires-Dist: arviz>=1; extra == "bayes"
Requires-Dist: xarray>=2023; extra == "bayes"
Requires-Dist: numpyro>=0.15; extra == "bayes"
Requires-Dist: blackjax>=1; extra == "bayes"
Requires-Dist: fast-histogram>=0.11; extra == "bayes"
Requires-Dist: h5netcdf[h5py]>=1.3; extra == "bayes"
Provides-Extra: asteca
Requires-Dist: asteca>=0.7; extra == "asteca"
Provides-Extra: selection
Requires-Dist: gaiaunlimited>=0.3; extra == "selection"
Requires-Dist: hr-selection-function>=1.0; extra == "selection"
Provides-Extra: paper
Requires-Dist: pymc>=6; extra == "paper"
Requires-Dist: arviz>=1; extra == "paper"
Requires-Dist: xarray>=2023; extra == "paper"
Requires-Dist: numpyro>=0.15; extra == "paper"
Requires-Dist: blackjax>=1; extra == "paper"
Requires-Dist: asteca>=0.6; extra == "paper"
Requires-Dist: seaborn>=0.12; extra == "paper"
Requires-Dist: fast-histogram>=0.11; extra == "paper"
Requires-Dist: h5netcdf[h5py]>=1.3; extra == "paper"
Dynamic: license-file

# EROTICA: Estimation, Recovery & Optimization, together with Inference, for Cluster Analysis

[![Python 3.13+](https://img.shields.io/badge/python-3.13+-blue.svg)](https://www.python.org/downloads/)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0)
[![Development Status](https://img.shields.io/badge/status-alpha-red.svg)](https://github.com/notluquis/erotica)
[![CI](https://github.com/notluquis/erotica/actions/workflows/ci.yml/badge.svg)](https://github.com/notluquis/erotica/actions/workflows/ci.yml)
[![Docs](https://readthedocs.org/projects/erotica/badge/?version=latest)](https://erotica.readthedocs.io/en/latest/)
[![PyPI](https://img.shields.io/pypi/v/erotica.svg)](https://pypi.org/project/erotica/)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21769959.svg)](https://doi.org/10.5281/zenodo.21769959)

EROTICA is a Python package for analyzing star clusters using machine learning and Bayesian
inference. Built for Gaia data, it uses unsupervised clustering to identify open star clusters
and characterize their membership, ages, and structure.

📖 **[Documentation](https://erotica.readthedocs.io/en/latest/)** · [Membership guide](https://erotica.readthedocs.io/en/latest/guides/membership.html) · [API reference](https://erotica.readthedocs.io/en/latest/api/index.html)

## 📦 Installation

```bash
pip install erotica
```

### From source (for development)

```bash
git clone https://github.com/notluquis/erotica.git
cd erotica
pip install -e ".[dev,docs]"
```

### Requirements

- Python 3.13 or higher (this is `requires-python`, not a recommendation — older versions cannot
  install the package)
- See `pyproject.toml` for the complete dependency list and the optional extras
  (`bayes`, `selection`, `examples`, `docs`, `dev`)

## 🚀 Quick Start

```python
import erotica

# Load and preprocess
loader = erotica.DataLoader("your_gaia_catalog.ecsv")
data = loader.load_data(systems=["Gaia", "TMASS"])

preprocessor = erotica.DataPreprocessor(data)
preprocessor.apply_zero_point_correction()
good_data, bad_data = preprocessor.filter_data(fidelity_threshold=0.5)

# Membership: sweep min_cluster_size, score by recovery frequency x strength.
# `columns` IS the feature-space choice — 2D proper motion is the safe default.
# Mixing units (adding parallax) requires standardizing the columns first;
# see the membership guide for the trade-offs.
clusterer = erotica.Clustering(good_data, bad_data)
clusterer.search_pseudoprobability(columns=("pmra", "pmdec"))

clusterer.clustering_statistics()
summary = clusterer.get_cluster_summary()
clusterer.save_results("members.ecsv", format="ascii.ecsv")
```

Downstream analysis (isochrone fitting, structure, dynamics) goes through
`ClusterAnalyzer`, which is constructed from a saved catalog — see the
[quickstart](https://erotica.readthedocs.io/en/latest/quickstart.html).

## 🏗️ Project Structure

```
erotica/
├── erotica/               # 📦 Main package
│   ├── core/              # 🔧 HDBSCAN clustering + membership
│   ├── io/                # 📊 Data loading and I/O
│   ├── preprocess/        # 🧹 Data preprocessing
│   ├── analysis/          # 📈 Isochrones, structure, dynamics, inference
│   │   ├── structure.py       #   radial profiles: King, EFF, Plummer, King+corona
│   │   ├── identifiability.py #   is this parameter measured, or is the prior answering?
│   │   ├── synthetic.py       #   fractal cluster generation, for validation
│   │   └── provenance.py      #   git SHA, checksums, seeds, dependency versions
│   ├── selection.py       # 🔭 Gaia DR3 selection function
│   ├── calibration.py     # 🎯 Probability calibration
│   └── utils/             # 🛠️ Utility functions
├── docs/                  # 📚 Sphinx documentation
│   ├── guides/            # User guides (membership, …)
│   ├── design-notes/      # Grounded method notes
│   └── api/               # Auto-generated API reference
├── tools/                 # 🔧 Development + release tooling
│   └── validation/        #   every quoted number has a script here
├── data/test/NGC6383/     # 🔬 Paper reproduction artifacts
└── tests/                 # 🧪 Test suite
```

## 🌟 What it does

### Radial structure, without binning
Profiles are fitted as an **unbinned inhomogeneous Poisson point process**, `Σ log λ(rᵢ) − Λ`, so
there is no bin width to choose and no Gaussian approximation to break in the sparse outer bins that
set the truncation radius. Four families share the machinery and are scored against each other by
SMC Bayes factor:

| model | when |
|---|---|
| **King (1962)** | the empirical standard. Note the additive background is *not* King's — see the docstring |
| **EFF** (Elson, Fall & Freeman 1987) | no tidal cutoff; the honest model when truncation is not locatable |
| **Plummer** | `EFF(γ = 4)` |
| **King + uniform-sphere corona** (Danilov & Putkov 2012; Seleznev 2016) | when the flat background is absorbing cluster structure rather than field |

Each carries a closed-form normalisation verified against quadrature, because the integral is
evaluated at every leapfrog step.

### Asking whether a number means anything
`erotica.analysis.identifiability` answers *"is this parameter measured, or is the prior answering for
it?"* — **before** it is quoted. Four angles, because a parameter can pass one and fail another:
power-scaling prior sensitivity (Kallioinen et al. 2024), posterior geometry and condition number,
relative width, and the Muñoz et al. (2012) geometric criteria. It exists because this project
repeatedly reported numbers the data did not determine and found out late.

### Membership and calibration
HDBSCAN with persistence thresholding and a hyperparameter sweep scored by recovery frequency ×
strength. The resulting pseudo-probability is **calibrated against an external benchmark** rather than
assumed to be a probability, and a target–decoy construction measures the false-discovery proportion
independently of the fit.

### Selection functions
Gaia DR3 completeness via `gaiaunlimited`, foldable directly into the point-process normalisation, so
a radial completeness gradient enters the likelihood rather than being corrected afterwards.

### Synthetic clusters for validation, not for science
`fractal_cluster` implements the Goodwin & Whitworth (2004) box-fractal construction — still the live
standard in 2026 — because drawing test data from a smooth profile begs the question when the thing
under test *is* a profile fit.

### Reproducibility
`build_metadata` records git SHA and dirty flag, input-data checksums, RNG seeds, sampler
configuration and dependency versions. A result without those is not reproducible, only repeatable.

## ⚠️ What it will not tell you

Stated up front because the alternative is a referee stating it:

- **A tidal radius is often prior-determined.** For a footprint that does not contain the cluster,
  `R_t` is unconstrained regardless of how well the sampler converged. Any `R_t` must name its prior.
- **The EFF slope is not recoverable at typical Gaia-census geometry.** The controlling variable is
  the footprint-to-scale ratio, not the sample size.
- **Circular symmetry is assumed and is the exception** — median core axis ratio **0.71**, computed
  here from Tarricq et al. (2022)'s published table, not quoted from their text (they report the
  distribution as peaking at 0.8–0.9).
- **On a membership-selected sample the background term is not contamination.** It absorbs the
  corona, and for one cluster it accounts for 56% of the sample against a 6.1% measured
  false-discovery rate.

Each of these is measured, scripted under `tools/validation/`, and written up in
`docs/design-notes/`.

## 🔭 How it relates to other packages

Written once because three reviewers ask for it: pyOpenSci's review template asks for a comparison to
similar packages, JOSS requires a **State of the field** section, and Astropy's affiliated criteria
ask that a package avoid duplicating existing functionality.

**Supported Python: 3.13+** (CI runs 3.13 and 3.14).

| package | what it does that EROTICA does not | overlap |
|---|---|---|
| [**ASteCA**](https://github.com/asteca/ASteCA) | The standard for **photometric** synthetic clusters and CMD fitting: IMF, mass-dependent binary fraction fitted to Offner et al. (2023), extinction with free `R_V`, differential reddening, errors calibrated on the observed cluster. **Cite it; do not compete with it.** | membership; a private, unwired least-squares King fit |
| [**ocelot**](https://github.com/emilyhunt/ocelot) | Injects mock clusters into real Gaia DR3 with a two-stage selection function and error twin-resampling — 80,590 injection/retrievals behind the census completeness function. | radial profiles (it ships King62 only) |
| [**Kalkayotl**](https://github.com/olivares-j/Kalkayotl) | Bayesian cluster distances with **parallax spatial correlations** (Lindegren et al. 2020), which EROTICA does not model. The strongest error model in the Bayesian open-cluster set. | distance inference |
| [**gaiaunlimited**](https://github.com/gaia-unlimited/gaiaunlimited) | The Gaia DR3 selection function itself. EROTICA **uses** it rather than reimplementing it. | — |
| [**pyUPMASK**](https://github.com/msolpera/pyUPMASK) / UPMASK | Membership by iterative clustering plus a random-field null. | membership |
| [**SPISEA**](https://github.com/astropy/SPISEA) | Stellar population synthesis: IMF, multiplicity, extinction, IFMR. No spatial structure. | — |

**What is distinctive here**, stated narrowly enough to survive checking:

- Radial profiles as an **unbinned inhomogeneous Poisson point process**, with King, EFF, Plummer and
  King-plus-corona compared by Bayes factor under one likelihood.
- A **selection function folded into the normalisation** rather than applied as an after-the-fact
  correction.
- An **identifiability toolkit** that asks whether a parameter is measured before it is quoted.

**What is not distinctive, and is documented as such**: Bayesian King fitting is not new — Pera et al.
(2021, 2024) precede it. EFF fits to Galactic open clusters as a population are not new either;
Cordoni et al. (2023) fit 78 and Pang et al. (2022) fit 82. Claims of novelty in this project have
been falsified seven times, so they are checked before they are written.

## 📖 Documentation

Full documentation: **[erotica.readthedocs.io](https://erotica.readthedocs.io/en/latest/)**

> **Resolved 2026-08-03.** This block used to warn that the Read the Docs slug was still
> `cosmic-clusters` and that `erotica.readthedocs.io` would 404. That is no longer true, and the
> paragraph had also been corrupted by the rename pass — it printed the *same* URL on both sides of
> "while", so it read as a contradiction and then instructed future editors not to fix it.
> Measured directly: `erotica.readthedocs.io/en/latest/` → **HTTP 200**,
> `cosmic-clusters.readthedocs.io/en/latest/` → **HTTP 404**. The link above is correct; use it.

### Getting started
- [Installation](https://erotica.readthedocs.io/en/latest/install.html)
- [Quickstart](https://erotica.readthedocs.io/en/latest/quickstart.html)

### Guides
- [Membership](https://erotica.readthedocs.io/en/latest/guides/membership.html) —
  which features to cluster on, error-aware membership, calibration, and the trade-offs of each choice

### Reference
- [API reference](https://erotica.readthedocs.io/en/latest/api/index.html)
- [Design notes](https://erotica.readthedocs.io/en/latest/design-notes/index.html) —
  grounded notes on the isochrone sampler, model grids, and HDBSCAN membership

Build the docs locally with `pip install -e ".[docs]" && sphinx-build -b html docs docs/_build/html`.

## 🔧 Development

### Quick Setup

```bash
# Automated development environment setup
python tools/dev/setup_environment.py
```

### Manual Setup

```bash
# Install in development mode
pip install -e ".[dev,docs]"

# Set up pre-commit hooks
pre-commit install

# Run tests
pytest

# Run comprehensive test suite
python tools/testing/run_comprehensive_tests.py
```

### Code Quality

**`ruff` replaced `black` + `isort` + `flake8`.** It is what `.pre-commit-config.yaml` actually runs,
so it is what gates a commit; the three tools this section used to name are no longer installed by
the `dev` extra.

```bash
# Lint and autofix, then format — the same two hooks pre-commit runs
ruff check --fix erotica/ tests/
ruff format erotica/ tests/

# Types (deliberately NOT a pre-commit hook — too slow for the commit loop)
mypy erotica/ --ignore-missing-imports

# Or just install the hooks and let them run on every commit
pre-commit install
```

## 📊 Status

**Current version**: **v0.2.0** (2026-09-22) — see `CHANGELOG.md` for the full list of changes
since v0.1.0 (released 2026-08-03, [PyPI](https://pypi.org/project/erotica/), Zenodo concept DOI
[`10.5281/zenodo.21769959`](https://doi.org/10.5281/zenodo.21769959), which always resolves to the
latest archived version). This block claimed "v0.0.1 (Alpha) — active development for v0.1.0"
until 2026-08-04, when `pyproject.toml`, `CITATION.cff` and `CHANGELOG.md` had all said 0.1.0 for
a day; keep the three in step on every release.

**Development status**: v0.2.0 is the version the JOSS submission describes. The API is not yet
frozen; see the roadmap below.

**Tests**: the full suite runs in CI on Python 3.13 and 3.14, plus a job that installs the `bayes`
extra so the sampler paths actually execute, and a docs job that builds the site. Audited by
mutation rather than by coverage. (No test count is quoted here on purpose — a count in prose is
stale by the next commit. Run `pytest --collect-only -q` for the current figure; `paper/paper.md`
quotes one pinned to the v0.2.0 release, which is fixed and citable.)

**Documentation**: [erotica.readthedocs.io](https://erotica.readthedocs.io/en/latest/) — API
reference, user guides and design notes.

## 🤝 Contributing

We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.

### Quick Contribution Steps

1. Fork the repository
2. Create a feature branch: `git checkout -b feature/amazing-feature`
3. Make your changes and add tests
4. Run the test suite: `pytest -q` (what CI runs, and what `CONTRIBUTING.md` says)
5. Commit your changes: `git commit -m 'Add amazing feature'`
6. Push to the branch: `git push origin feature/amazing-feature`
7. Open a Pull Request

## 📣 Citing EROTICA

If EROTICA contributes to work you publish, please cite it. Machine-readable metadata is in
[`CITATION.cff`](CITATION.cff), which GitHub renders as a "Cite this repository" button.

```bibtex
@software{erotica,
  author  = {Pulgar-Escobar, Lucas and Cerulo, Pierluigi},
  title   = {{EROTICA}: Estimation, Recovery \& Optimization, together with Inference,
             for Cluster Analysis},
  version = {0.2.0},
  year    = {2026},
  doi     = {10.5281/zenodo.21769959},
  url     = {https://github.com/notluquis/erotica},
  license = {AGPL-3.0-or-later}
}
```

The DOI above is the **concept** DOI and always resolves to the latest archived version — cite it
if you want "whichever version I ran." Every archived version also gets its own **version** DOI
to pin one exactly; v0.1.0's is `10.5281/zenodo.21769960`. `CITATION.cff` is the source of truth
for which version DOI matches which release. The author field is spelled to match
`CITATION.cff` — it read `Pulgar, Lucas M.` until 2026-08-04, so the two files disagreed on the
family name and would have produced two different citations for one release.

> A software paper describing EROTICA is prepared for submission to the Journal of Open
> Source Software ([`paper/paper.md`](paper/paper.md)). Once it is accepted and has a DOI,
> `CITATION.cff`'s `preferred-citation` field will redirect GitHub's citation button to it
> and supersede this entry.

**Please also cite the methods you actually used**, which are not ours: King (1962) or Elson, Fall &
Freeman (1987) for the profile you fitted, Hunt & Reffert (2024) for census cross-matches,
Cantat-Gaudin et al. (2023) via `gaiaunlimited` for the selection function, Kallioinen et al. (2024)
for the power-scaling diagnostics, and Goodwin & Whitworth (2004) for the synthetic generator. Each
is cited at the point of use in the API documentation.

## 📄 License

EROTICA is licensed under the [GNU Affero General Public License v3.0](LICENSE). This ensures that any modifications or derivative works remain open source.

## 👥 Authors

**Lucas Pulgar-Escobar** — Universidad de Concepción, Chile
([lescobar2019@udec.cl](mailto:lescobar2019@udec.cl), ORCID
[0009-0008-4359-2444](https://orcid.org/0009-0008-4359-2444)) — sole implementer.

**Pierluigi Cerulo** — Departamento de Ingeniería Informática y Ciencias de la Computación,
Universidad de Concepción, Chile (ORCID
[0000-0003-0703-3123](https://orcid.org/0000-0003-0703-3123)) — project direction.

Matches the authors of [`paper/paper.md`](paper/paper.md) and, since 2026-09-22,
[`CITATION.cff`](CITATION.cff)'s `authors:` list — the Zenodo/software-archive author list — in
the same order. The two lists disagreed (Pulgar-Escobar alone in `CITATION.cff`) between
2026-08-03 and 2026-09-22; see that file's own comments for when and why Cerulo was added there.

## 🙏 Acknowledgments

We thank **Nicolás Henríquez Salgado** (Universidad de Concepción) for his contribution to the
companion NGC 6383 science paper.

We gratefully acknowledge support from the ANID BASAL project FB210003 and the SOCHIAS GEMINI
project 32230014. L.P.-E. acknowledges financial support from the Dirección de Postgrado,
Universidad de Concepción, through its MSc scholarship programme. P.C. acknowledges support
from the UdeC VRID Iniciación fund 2025001386INI. The funders had no role in the design of the
software, the analyses, or the writing of the paper.

EROTICA builds upon excellent open-source libraries:
- [HDBSCAN](https://github.com/scikit-learn-contrib/hdbscan) for density-based clustering
- [Optuna](https://optuna.org/) for hyperparameter optimization
- [Astropy](https://www.astropy.org/) for astronomical data handling
- [scikit-learn](https://scikit-learn.org/) for machine learning utilities
- [Matplotlib](https://matplotlib.org/) for visualization

## 📞 Support

- **Documentation**: [erotica.readthedocs.io](https://erotica.readthedocs.io/en/latest/)
- **Issues**: [GitHub Issues](https://github.com/notluquis/erotica/issues)
- **Discussions**: [GitHub Discussions](https://github.com/notluquis/erotica/discussions)
- **Email**: [lescobar2019@udec.cl](mailto:lescobar2019@udec.cl)

## 🗺️ Roadmap

- [x] **v0.0.1** — Initial alpha release with core functionality
- [x] **v0.1.0** — Comprehensive documentation, Zenodo archive, **and PyPI distribution**
      (which this list had parked under v0.2.0; `pip install erotica` works today)
- [x] **v0.2.0** — Cluster-selector correctness fixes (BREAKING default), unified
      membership-column semantics, a marginalized (no longer funnel-prone) `distance_model`,
      and a second author; see `CHANGELOG.md`.
- [ ] **v0.3.0** — Additional clustering algorithms; a frozen, deprecation-policed API
- [ ] **v1.0.0** — Production-ready release with full validation suite

---

**Citation**: If you use EROTICA in your research, please cite it via [`CITATION.cff`](CITATION.cff) (see "Citing EROTICA" above) and acknowledge the underlying libraries.

*Made with ❤️ for the astronomical community*
