flodr 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flodr-0.1.0/.gitignore +231 -0
- flodr-0.1.0/LICENSE +21 -0
- flodr-0.1.0/PKG-INFO +240 -0
- flodr-0.1.0/README.md +176 -0
- flodr-0.1.0/pyproject.toml +84 -0
- flodr-0.1.0/src/flodr/__init__.py +38 -0
- flodr-0.1.0/src/flodr/data.py +238 -0
- flodr-0.1.0/src/flodr/estimator.py +860 -0
- flodr-0.1.0/src/flodr/model.py +762 -0
- flodr-0.1.0/src/flodr/train.py +982 -0
- flodr-0.1.0/src/flodr/viz.py +497 -0
flodr-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
*.lcov
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
cover/
|
|
54
|
+
|
|
55
|
+
# Translations
|
|
56
|
+
*.mo
|
|
57
|
+
*.pot
|
|
58
|
+
|
|
59
|
+
# Django stuff:
|
|
60
|
+
*.log
|
|
61
|
+
local_settings.py
|
|
62
|
+
db.sqlite3
|
|
63
|
+
db.sqlite3-journal
|
|
64
|
+
|
|
65
|
+
# Flask stuff:
|
|
66
|
+
instance/
|
|
67
|
+
.webassets-cache
|
|
68
|
+
|
|
69
|
+
# Scrapy stuff:
|
|
70
|
+
.scrapy
|
|
71
|
+
|
|
72
|
+
# Sphinx documentation
|
|
73
|
+
docs/_build/
|
|
74
|
+
|
|
75
|
+
# PyBuilder
|
|
76
|
+
.pybuilder/
|
|
77
|
+
target/
|
|
78
|
+
|
|
79
|
+
# Jupyter Notebook
|
|
80
|
+
.ipynb_checkpoints
|
|
81
|
+
|
|
82
|
+
# IPython
|
|
83
|
+
profile_default/
|
|
84
|
+
ipython_config.py
|
|
85
|
+
|
|
86
|
+
# pyenv
|
|
87
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
88
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
89
|
+
# .python-version
|
|
90
|
+
|
|
91
|
+
# pipenv
|
|
92
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
93
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
94
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
95
|
+
# install all needed dependencies.
|
|
96
|
+
# Pipfile.lock
|
|
97
|
+
|
|
98
|
+
# UV
|
|
99
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
100
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
101
|
+
# commonly ignored for libraries.
|
|
102
|
+
# uv.lock
|
|
103
|
+
|
|
104
|
+
# poetry
|
|
105
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
106
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
107
|
+
# commonly ignored for libraries.
|
|
108
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
109
|
+
# poetry.lock
|
|
110
|
+
# poetry.toml
|
|
111
|
+
|
|
112
|
+
# pdm
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
114
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
115
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
116
|
+
# pdm.lock
|
|
117
|
+
# pdm.toml
|
|
118
|
+
.pdm-python
|
|
119
|
+
.pdm-build/
|
|
120
|
+
|
|
121
|
+
# pixi
|
|
122
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
123
|
+
# pixi.lock
|
|
124
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
125
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
126
|
+
.pixi/*
|
|
127
|
+
!.pixi/config.toml
|
|
128
|
+
|
|
129
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
130
|
+
__pypackages__/
|
|
131
|
+
|
|
132
|
+
# Celery stuff
|
|
133
|
+
celerybeat-schedule*
|
|
134
|
+
celerybeat.pid
|
|
135
|
+
|
|
136
|
+
# Redis
|
|
137
|
+
*.rdb
|
|
138
|
+
*.aof
|
|
139
|
+
*.pid
|
|
140
|
+
|
|
141
|
+
# RabbitMQ
|
|
142
|
+
mnesia/
|
|
143
|
+
rabbitmq/
|
|
144
|
+
rabbitmq-data/
|
|
145
|
+
|
|
146
|
+
# ActiveMQ
|
|
147
|
+
activemq-data/
|
|
148
|
+
|
|
149
|
+
# SageMath parsed files
|
|
150
|
+
*.sage.py
|
|
151
|
+
|
|
152
|
+
# Environments
|
|
153
|
+
.env
|
|
154
|
+
.envrc
|
|
155
|
+
.venv
|
|
156
|
+
env/
|
|
157
|
+
venv/
|
|
158
|
+
ENV/
|
|
159
|
+
env.bak/
|
|
160
|
+
venv.bak/
|
|
161
|
+
|
|
162
|
+
# Spyder project settings
|
|
163
|
+
.spyderproject
|
|
164
|
+
.spyproject
|
|
165
|
+
|
|
166
|
+
# Rope project settings
|
|
167
|
+
.ropeproject
|
|
168
|
+
|
|
169
|
+
# mkdocs documentation
|
|
170
|
+
/site
|
|
171
|
+
|
|
172
|
+
# mypy
|
|
173
|
+
.mypy_cache/
|
|
174
|
+
.dmypy.json
|
|
175
|
+
dmypy.json
|
|
176
|
+
|
|
177
|
+
# Pyre type checker
|
|
178
|
+
.pyre/
|
|
179
|
+
|
|
180
|
+
# pytype static type analyzer
|
|
181
|
+
.pytype/
|
|
182
|
+
|
|
183
|
+
# Cython debug symbols
|
|
184
|
+
cython_debug/
|
|
185
|
+
|
|
186
|
+
# PyCharm
|
|
187
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
188
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
189
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
190
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
191
|
+
# .idea/
|
|
192
|
+
|
|
193
|
+
# Abstra
|
|
194
|
+
# Abstra is an AI-powered process automation framework.
|
|
195
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
196
|
+
# Learn more at https://abstra.io/docs
|
|
197
|
+
.abstra/
|
|
198
|
+
|
|
199
|
+
# Visual Studio Code
|
|
200
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
201
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
202
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
203
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
204
|
+
# .vscode/
|
|
205
|
+
# Temporary file for partial code execution
|
|
206
|
+
tempCodeRunnerFile.py
|
|
207
|
+
|
|
208
|
+
# Ruff stuff:
|
|
209
|
+
.ruff_cache/
|
|
210
|
+
|
|
211
|
+
# PyPI configuration file
|
|
212
|
+
.pypirc
|
|
213
|
+
|
|
214
|
+
# Marimo
|
|
215
|
+
marimo/_static/
|
|
216
|
+
marimo/_lsp/
|
|
217
|
+
__marimo__/
|
|
218
|
+
|
|
219
|
+
# Streamlit
|
|
220
|
+
.streamlit/secrets.toml
|
|
221
|
+
|
|
222
|
+
*.pdf
|
|
223
|
+
paper/
|
|
224
|
+
cache/
|
|
225
|
+
tables/
|
|
226
|
+
|
|
227
|
+
data/
|
|
228
|
+
|
|
229
|
+
figures/
|
|
230
|
+
|
|
231
|
+
experiments/
|
flodr-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Probst
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
flodr-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: flodr
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FloDR: exactly invertible dimensionality reduction built on a normalizing flow
|
|
5
|
+
Author-email: Daniel Probst <daenuprobst@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Daniel Probst
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Python: >=3.11
|
|
29
|
+
Requires-Dist: matplotlib>=3.7
|
|
30
|
+
Requires-Dist: numpy>=1.26
|
|
31
|
+
Requires-Dist: scikit-learn>=1.6
|
|
32
|
+
Requires-Dist: scipy>=1.11
|
|
33
|
+
Requires-Dist: threadpoolctl>=3.1
|
|
34
|
+
Requires-Dist: torch>=2.1
|
|
35
|
+
Requires-Dist: tqdm>=4.66
|
|
36
|
+
Requires-Dist: typing-extensions>=4.15
|
|
37
|
+
Requires-Dist: usearch>=2.25.3
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: black>=24.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: pandas>=2.0; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
42
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
43
|
+
Provides-Extra: notebook
|
|
44
|
+
Requires-Dist: ipykernel>=6.29; extra == 'notebook'
|
|
45
|
+
Requires-Dist: nbconvert>=7.0; extra == 'notebook'
|
|
46
|
+
Requires-Dist: nbformat>=5.9; extra == 'notebook'
|
|
47
|
+
Provides-Extra: paper
|
|
48
|
+
Requires-Dist: aiohttp>=3.9; extra == 'paper'
|
|
49
|
+
Requires-Dist: anndata>=0.10; extra == 'paper'
|
|
50
|
+
Requires-Dist: drfp>=0.3; extra == 'paper'
|
|
51
|
+
Requires-Dist: faiss-cpu>=1.8; extra == 'paper'
|
|
52
|
+
Requires-Dist: fsspec>=2024.2; extra == 'paper'
|
|
53
|
+
Requires-Dist: opentsne>=1.0; extra == 'paper'
|
|
54
|
+
Requires-Dist: pacmap>=0.7; extra == 'paper'
|
|
55
|
+
Requires-Dist: pandas>=2.0; extra == 'paper'
|
|
56
|
+
Requires-Dist: phate>=1.0; extra == 'paper'
|
|
57
|
+
Requires-Dist: pillow>=10.0; extra == 'paper'
|
|
58
|
+
Requires-Dist: pymde>=0.1; extra == 'paper'
|
|
59
|
+
Requires-Dist: scanpy>=1.10; extra == 'paper'
|
|
60
|
+
Requires-Dist: trimap>=1.1; extra == 'paper'
|
|
61
|
+
Requires-Dist: umap-learn>=0.5; extra == 'paper'
|
|
62
|
+
Requires-Dist: umato>=1.0; extra == 'paper'
|
|
63
|
+
Description-Content-Type: text/markdown
|
|
64
|
+
|
|
65
|
+
# FloDR
|
|
66
|
+
|
|
67
|
+
Dimensionality reduction built on an invertible normalising flow. The first
|
|
68
|
+
`n_components` output coordinates of the flow are the embedding and the rest are kept as
|
|
69
|
+
a residual, so the trained map is a bijection with an exact inverse. The conditional spread
|
|
70
|
+
uses that inverse to measure what the layout leaves undetermined at each position. The API
|
|
71
|
+
follows the scikit-learn estimator interface. The documentation is at
|
|
72
|
+
https://daenuprobst.github.io/flodr/.
|
|
73
|
+
|
|
74
|
+

|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
Python 3.11 or newer.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pip install flodr
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For development, install from a clone with the locked dependencies. The tests run on the CPU.
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/daenuprobst/flodr
|
|
88
|
+
cd flodr
|
|
89
|
+
uv sync --extra dev
|
|
90
|
+
uv run pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
import numpy as np
|
|
97
|
+
from flodr import FloDR, viz
|
|
98
|
+
|
|
99
|
+
X = np.random.default_rng(0).normal(size=(2000, 50)).astype("float32")
|
|
100
|
+
labels = (X[:, 0] > 0).astype(int)
|
|
101
|
+
|
|
102
|
+
model = FloDR(n_components=2, random_state=0).fit(X)
|
|
103
|
+
Y = model.embedding_ # (n, 2)
|
|
104
|
+
|
|
105
|
+
Y_new = model.transform(X[:10]) # one forward pass through the flow
|
|
106
|
+
Y_opt = model.transform_opt(X[:10]) # per-point placement, UMAP-style
|
|
107
|
+
X_back = model.inverse_transform(Y[:10]) # embedding back to input space
|
|
108
|
+
Z = model.transform_latent(X[:10]) # layout plus residual
|
|
109
|
+
X_same = model.inverse_latent(Z) # exact to float precision
|
|
110
|
+
logp = model.score_samples(X[:10]) # exact log density
|
|
111
|
+
|
|
112
|
+
sigma = model.conditional_spread() # what the layout leaves undetermined
|
|
113
|
+
field, cert = model.hidden_contrast(labels) # label structure the layout hides
|
|
114
|
+
|
|
115
|
+
ax = viz.plot_embedding(Y, labels)
|
|
116
|
+
viz.save(ax.figure, "embedding.pdf")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Training runs on the GPU whenever CUDA is available; pass `device="cpu"` to force the
|
|
120
|
+
CPU. `hidden_contrast` fits a classifier per permutation replica and is by far the
|
|
121
|
+
slowest call above.
|
|
122
|
+
|
|
123
|
+
`FloDR` is a scikit-learn transformer, so it clones, sits in a `Pipeline`, and returns
|
|
124
|
+
pandas frames with `set_output(transform="pandas")` (columns `flodr0`, `flodr1`, ...). `score`
|
|
125
|
+
is the mean log density per sample, which `GridSearchCV` uses on held-out folds when no
|
|
126
|
+
scorer is given. A fitted model pickles with its tensors on the CPU, so a GPU fit loads on a
|
|
127
|
+
machine without one. `fit_transform` returns the training layout; inputs under 30 features
|
|
128
|
+
carry padding columns that new points do not, so there `transform` on the training data
|
|
129
|
+
places points near, not on, `embedding_`.
|
|
130
|
+
|
|
131
|
+
## Parameters
|
|
132
|
+
|
|
133
|
+
| Parameter | Default | Description |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `n_components` | `2` | Embedding dimension. Must be at least 2 and below the latent dimension. |
|
|
136
|
+
| `w` | `2.0` | Weight of the ordinal term, the local-to-global dial. Higher keeps more global distance. |
|
|
137
|
+
| `density` | `True` | Fit the residual density. Required by `score_samples`, `score` and the diagnostics. |
|
|
138
|
+
| `max_iter` | `None` | Training steps. `None` keeps the recipe's 6400, see Training budget. |
|
|
139
|
+
| `random_state` | `0` | Seed for the fit. `None` draws one. |
|
|
140
|
+
| `device` | GPU if there is one | Resolved by `flodr.default_device()`. Pass `"cpu"` to force it. |
|
|
141
|
+
| `verbose` | `True` | Draw a tqdm bar while training. |
|
|
142
|
+
| `advanced` | `None` | Dict of `TrainConfig` overrides, e.g. `dict(pad=0, fine_eps=0, edge_batch=32768)`. Unknown keys raise. |
|
|
143
|
+
|
|
144
|
+
## Methods
|
|
145
|
+
|
|
146
|
+
| Method | Returns | Description |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `fit(X)` / `fit_transform(X)` | self / `(n, k)` | Fit the flow; the layout also lands in `embedding_`. |
|
|
149
|
+
| `transform(X)` | `(m, k)` | Embed new points in one forward pass. |
|
|
150
|
+
| `transform_opt(X, steps=200, lr=0.05)` | `(m, k)` | Placement transform: per-point optimisation against the frozen train embedding, in the same class as UMAP's `transform`. |
|
|
151
|
+
| `inverse_transform(Y)` | `(m, d)` | Decode screen positions, residual set to zero. |
|
|
152
|
+
| `transform_latent(X)` | `(m, D)` | Full latent coordinates: the layout, then the residual. |
|
|
153
|
+
| `inverse_latent(Z)` | `(m, d)` | Decode full latent coordinates, exactly. |
|
|
154
|
+
| `score_samples(X)` | `(m,)` | Log density under the flow. |
|
|
155
|
+
| `score(X)` | float | Mean of `score_samples`. |
|
|
156
|
+
| `conditional_spread(Y=None, n_samples=64)` | `(m,)` | Spread of the fiber over each position, in input units. |
|
|
157
|
+
| `atypicality(n_samples=128)` | `(n,)` | Per-point tail probability under its own fiber, in `[0, 1]`. |
|
|
158
|
+
| `spread_calibration(n_bins=14, min_pts=12, n_samples=64)` | dict | Certificate for `conditional_spread`, with `passed` and `confidence`. |
|
|
159
|
+
| `hidden_contrast(G, ...)` | `(field, cert)` | Label information present in the input but not in the layout, in nats, against a within-bin permutation null (`n_perm=99` by default). |
|
|
160
|
+
| `diagnostics(G=None, **kw)` | dict | Both fields with their certificates, units and labels. |
|
|
161
|
+
|
|
162
|
+
`fit(X, iters=..., progress=...)` and `inverse_coords(Z)` still work and warn; use
|
|
163
|
+
`max_iter`, `verbose` and `inverse_latent`.
|
|
164
|
+
|
|
165
|
+
## Attributes
|
|
166
|
+
|
|
167
|
+
| Attribute | Description |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `embedding_` | `(n, k)` layout of the training data. |
|
|
170
|
+
| `n_iter_` | Training steps run. |
|
|
171
|
+
| `n_features_in_`, `feature_names_in_` | Input width, and column names when fit on a DataFrame. |
|
|
172
|
+
| `roundtrip_` | Max absolute error of the latent round trip. |
|
|
173
|
+
| `flow_` | The trained `Flow`. |
|
|
174
|
+
| `knn_idx_`, `knn_dist_` | The 15-NN graph the fit was built on. |
|
|
175
|
+
|
|
176
|
+
## `flodr.viz`
|
|
177
|
+
|
|
178
|
+
| Function | Description |
|
|
179
|
+
|---|---|
|
|
180
|
+
| `plot_embedding(Y, labels)` | Scatter of the layout. |
|
|
181
|
+
| `plot_field(Y, v)` | One scalar field over the layout. |
|
|
182
|
+
| `bivariate_field(Y, x, y)` | Two fields in one panel, with a corner key. |
|
|
183
|
+
| `occupancy_field(Y)` | Grid axes and a soft mask of where the layout has data. |
|
|
184
|
+
| `depth_fog(Y)` | Draw order, colour weight and depth, for 3D scatters. |
|
|
185
|
+
| `residual(flow, X)`, `residual_fraction(flow, X)` | The residual coordinates and their share of the latent norm. |
|
|
186
|
+
| `save(fig, path)` | Save at 300 dpi with tight bounds. |
|
|
187
|
+
| `RC`, `RC_PAPER` | Matplotlib rc dicts for screen and for print. |
|
|
188
|
+
|
|
189
|
+
### Training budget
|
|
190
|
+
|
|
191
|
+
`max_iter=None` runs 6400 steps at a learning rate of 0.0025. Setting it rescales the rate
|
|
192
|
+
to keep their product at 16, so `max_iter=1600` runs at 0.01; below 800 steps the rate stays
|
|
193
|
+
at 0.02, since larger steps break the inverse (a round trip of 0.16 at 40 steps). Each step
|
|
194
|
+
trains on the whole kNN graph up to 262,144 edges and on a random batch of that many above
|
|
195
|
+
it, with 48 negatives per edge. Between 50,000 and 500,000 points only the heaviest quarter
|
|
196
|
+
of the graph is kept.
|
|
197
|
+
|
|
198
|
+
Two parts help most on narrow inputs such as a 10-dimensional scVI or scPoli latent. Inputs below
|
|
199
|
+
30 dimensions get unit-variance noise columns up to 30 (`pad`), which widen the residual the
|
|
200
|
+
layout is routed through; the kNN graph and the ordinal term stay on the input. A shift-only
|
|
201
|
+
branch on fixed random frequencies (`fine_eps`, on for every input) joins the couplings that
|
|
202
|
+
write the layout halfway through training. `advanced=dict(pad=0, fine_eps=0)` turns both off.
|
|
203
|
+
|
|
204
|
+
On 250,000 cells of the HNOCA scPoli latent (10 dimensions) recall@15 is 0.134, against
|
|
205
|
+
0.102 for UMAP (default parameters, PCA initialisation) and 0.044 for the earlier
|
|
206
|
+
recipe, with both certificates passing.
|
|
207
|
+
On 103,000 bone marrow cells (PCA-50, no padding) it goes from 0.017 to 0.076. The price is
|
|
208
|
+
time: about 6 minutes at 250,000 cells on an RTX 4070 Ti, against under one minute. Every number
|
|
209
|
+
in the paper was produced with the earlier recipe (800 steps at 0.02, 12 negatives, no padding).
|
|
210
|
+
|
|
211
|
+
On CUDA the loss of each step is compiled (`compile_edges`) and its hidden layers run in fp16
|
|
212
|
+
with loss scaling (`half_edges="fp16"`), the output layers and the Fourier phase in fp32.
|
|
213
|
+
Together they are 2.2x faster than the eager fp32 path at 250,000 and 103,000 cells, at the same
|
|
214
|
+
recall and certificates, but no longer bitwise reproducible across runs;
|
|
215
|
+
`advanced=dict(compile_edges=False, half_edges="")` restores the eager path. `half_edges="bf16"`
|
|
216
|
+
is no faster and loses 2.5 to 3.5% recall.
|
|
217
|
+
|
|
218
|
+
For new points prefer `transform_opt`. Fit on 200,000 HNOCA cells, 50,000 held-out cells
|
|
219
|
+
reach recall@15 0.113 with `transform_opt` in about 6 s, against 0.031 with `transform` and
|
|
220
|
+
0.061 for the earlier recipe's `transform_opt`.
|
|
221
|
+
|
|
222
|
+
`verbose=False` turns the tqdm bar off outright. Left on, it draws only when stderr is a
|
|
223
|
+
terminal, so piping to a log does not fill it with carriage returns.
|
|
224
|
+
|
|
225
|
+
Low-level pieces are exported for custom training: `Flow`, `Coupling`, `TrainConfig`,
|
|
226
|
+
`train_flodr`, `raw_recipe`, plus the data helpers `preprocess`, `raw_coords`,
|
|
227
|
+
`fuzzy_knn_graph`, `knn_search`, `global_pairs`, `find_ab`.
|
|
228
|
+
|
|
229
|
+
## Example notebook
|
|
230
|
+
|
|
231
|
+
`examples/quickstart.ipynb` runs every call above on Fashion-MNIST, 40000 images reduced to
|
|
232
|
+
a standardised PCA-50, and takes about four minutes on a GPU. It fits on four fifths of the
|
|
233
|
+
images, embeds the rest with both transforms, sweeps `inverse_transform` over a grid of
|
|
234
|
+
screen positions to decode the layout back into pictures, and ends on the two diagnostics
|
|
235
|
+
with their certificates, both of which pass at that sample size.
|
|
236
|
+
|
|
237
|
+
## Reproducing the paper
|
|
238
|
+
|
|
239
|
+
`scripts/regen` rebuilds every figure, table and number of the paper from the released library,
|
|
240
|
+
starting from the public data sets. See `scripts/regen/README.md`.
|
flodr-0.1.0/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# FloDR
|
|
2
|
+
|
|
3
|
+
Dimensionality reduction built on an invertible normalising flow. The first
|
|
4
|
+
`n_components` output coordinates of the flow are the embedding and the rest are kept as
|
|
5
|
+
a residual, so the trained map is a bijection with an exact inverse. The conditional spread
|
|
6
|
+
uses that inverse to measure what the layout leaves undetermined at each position. The API
|
|
7
|
+
follows the scikit-learn estimator interface. The documentation is at
|
|
8
|
+
https://daenuprobst.github.io/flodr/.
|
|
9
|
+
|
|
10
|
+

|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
Python 3.11 or newer.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install flodr
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
For development, install from a clone with the locked dependencies. The tests run on the CPU.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/daenuprobst/flodr
|
|
24
|
+
cd flodr
|
|
25
|
+
uv sync --extra dev
|
|
26
|
+
uv run pytest
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
import numpy as np
|
|
33
|
+
from flodr import FloDR, viz
|
|
34
|
+
|
|
35
|
+
X = np.random.default_rng(0).normal(size=(2000, 50)).astype("float32")
|
|
36
|
+
labels = (X[:, 0] > 0).astype(int)
|
|
37
|
+
|
|
38
|
+
model = FloDR(n_components=2, random_state=0).fit(X)
|
|
39
|
+
Y = model.embedding_ # (n, 2)
|
|
40
|
+
|
|
41
|
+
Y_new = model.transform(X[:10]) # one forward pass through the flow
|
|
42
|
+
Y_opt = model.transform_opt(X[:10]) # per-point placement, UMAP-style
|
|
43
|
+
X_back = model.inverse_transform(Y[:10]) # embedding back to input space
|
|
44
|
+
Z = model.transform_latent(X[:10]) # layout plus residual
|
|
45
|
+
X_same = model.inverse_latent(Z) # exact to float precision
|
|
46
|
+
logp = model.score_samples(X[:10]) # exact log density
|
|
47
|
+
|
|
48
|
+
sigma = model.conditional_spread() # what the layout leaves undetermined
|
|
49
|
+
field, cert = model.hidden_contrast(labels) # label structure the layout hides
|
|
50
|
+
|
|
51
|
+
ax = viz.plot_embedding(Y, labels)
|
|
52
|
+
viz.save(ax.figure, "embedding.pdf")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Training runs on the GPU whenever CUDA is available; pass `device="cpu"` to force the
|
|
56
|
+
CPU. `hidden_contrast` fits a classifier per permutation replica and is by far the
|
|
57
|
+
slowest call above.
|
|
58
|
+
|
|
59
|
+
`FloDR` is a scikit-learn transformer, so it clones, sits in a `Pipeline`, and returns
|
|
60
|
+
pandas frames with `set_output(transform="pandas")` (columns `flodr0`, `flodr1`, ...). `score`
|
|
61
|
+
is the mean log density per sample, which `GridSearchCV` uses on held-out folds when no
|
|
62
|
+
scorer is given. A fitted model pickles with its tensors on the CPU, so a GPU fit loads on a
|
|
63
|
+
machine without one. `fit_transform` returns the training layout; inputs under 30 features
|
|
64
|
+
carry padding columns that new points do not, so there `transform` on the training data
|
|
65
|
+
places points near, not on, `embedding_`.
|
|
66
|
+
|
|
67
|
+
## Parameters
|
|
68
|
+
|
|
69
|
+
| Parameter | Default | Description |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `n_components` | `2` | Embedding dimension. Must be at least 2 and below the latent dimension. |
|
|
72
|
+
| `w` | `2.0` | Weight of the ordinal term, the local-to-global dial. Higher keeps more global distance. |
|
|
73
|
+
| `density` | `True` | Fit the residual density. Required by `score_samples`, `score` and the diagnostics. |
|
|
74
|
+
| `max_iter` | `None` | Training steps. `None` keeps the recipe's 6400, see Training budget. |
|
|
75
|
+
| `random_state` | `0` | Seed for the fit. `None` draws one. |
|
|
76
|
+
| `device` | GPU if there is one | Resolved by `flodr.default_device()`. Pass `"cpu"` to force it. |
|
|
77
|
+
| `verbose` | `True` | Draw a tqdm bar while training. |
|
|
78
|
+
| `advanced` | `None` | Dict of `TrainConfig` overrides, e.g. `dict(pad=0, fine_eps=0, edge_batch=32768)`. Unknown keys raise. |
|
|
79
|
+
|
|
80
|
+
## Methods
|
|
81
|
+
|
|
82
|
+
| Method | Returns | Description |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `fit(X)` / `fit_transform(X)` | self / `(n, k)` | Fit the flow; the layout also lands in `embedding_`. |
|
|
85
|
+
| `transform(X)` | `(m, k)` | Embed new points in one forward pass. |
|
|
86
|
+
| `transform_opt(X, steps=200, lr=0.05)` | `(m, k)` | Placement transform: per-point optimisation against the frozen train embedding, in the same class as UMAP's `transform`. |
|
|
87
|
+
| `inverse_transform(Y)` | `(m, d)` | Decode screen positions, residual set to zero. |
|
|
88
|
+
| `transform_latent(X)` | `(m, D)` | Full latent coordinates: the layout, then the residual. |
|
|
89
|
+
| `inverse_latent(Z)` | `(m, d)` | Decode full latent coordinates, exactly. |
|
|
90
|
+
| `score_samples(X)` | `(m,)` | Log density under the flow. |
|
|
91
|
+
| `score(X)` | float | Mean of `score_samples`. |
|
|
92
|
+
| `conditional_spread(Y=None, n_samples=64)` | `(m,)` | Spread of the fiber over each position, in input units. |
|
|
93
|
+
| `atypicality(n_samples=128)` | `(n,)` | Per-point tail probability under its own fiber, in `[0, 1]`. |
|
|
94
|
+
| `spread_calibration(n_bins=14, min_pts=12, n_samples=64)` | dict | Certificate for `conditional_spread`, with `passed` and `confidence`. |
|
|
95
|
+
| `hidden_contrast(G, ...)` | `(field, cert)` | Label information present in the input but not in the layout, in nats, against a within-bin permutation null (`n_perm=99` by default). |
|
|
96
|
+
| `diagnostics(G=None, **kw)` | dict | Both fields with their certificates, units and labels. |
|
|
97
|
+
|
|
98
|
+
`fit(X, iters=..., progress=...)` and `inverse_coords(Z)` still work and warn; use
|
|
99
|
+
`max_iter`, `verbose` and `inverse_latent`.
|
|
100
|
+
|
|
101
|
+
## Attributes
|
|
102
|
+
|
|
103
|
+
| Attribute | Description |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `embedding_` | `(n, k)` layout of the training data. |
|
|
106
|
+
| `n_iter_` | Training steps run. |
|
|
107
|
+
| `n_features_in_`, `feature_names_in_` | Input width, and column names when fit on a DataFrame. |
|
|
108
|
+
| `roundtrip_` | Max absolute error of the latent round trip. |
|
|
109
|
+
| `flow_` | The trained `Flow`. |
|
|
110
|
+
| `knn_idx_`, `knn_dist_` | The 15-NN graph the fit was built on. |
|
|
111
|
+
|
|
112
|
+
## `flodr.viz`
|
|
113
|
+
|
|
114
|
+
| Function | Description |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `plot_embedding(Y, labels)` | Scatter of the layout. |
|
|
117
|
+
| `plot_field(Y, v)` | One scalar field over the layout. |
|
|
118
|
+
| `bivariate_field(Y, x, y)` | Two fields in one panel, with a corner key. |
|
|
119
|
+
| `occupancy_field(Y)` | Grid axes and a soft mask of where the layout has data. |
|
|
120
|
+
| `depth_fog(Y)` | Draw order, colour weight and depth, for 3D scatters. |
|
|
121
|
+
| `residual(flow, X)`, `residual_fraction(flow, X)` | The residual coordinates and their share of the latent norm. |
|
|
122
|
+
| `save(fig, path)` | Save at 300 dpi with tight bounds. |
|
|
123
|
+
| `RC`, `RC_PAPER` | Matplotlib rc dicts for screen and for print. |
|
|
124
|
+
|
|
125
|
+
### Training budget
|
|
126
|
+
|
|
127
|
+
`max_iter=None` runs 6400 steps at a learning rate of 0.0025. Setting it rescales the rate
|
|
128
|
+
to keep their product at 16, so `max_iter=1600` runs at 0.01; below 800 steps the rate stays
|
|
129
|
+
at 0.02, since larger steps break the inverse (a round trip of 0.16 at 40 steps). Each step
|
|
130
|
+
trains on the whole kNN graph up to 262,144 edges and on a random batch of that many above
|
|
131
|
+
it, with 48 negatives per edge. Between 50,000 and 500,000 points only the heaviest quarter
|
|
132
|
+
of the graph is kept.
|
|
133
|
+
|
|
134
|
+
Two parts help most on narrow inputs such as a 10-dimensional scVI or scPoli latent. Inputs below
|
|
135
|
+
30 dimensions get unit-variance noise columns up to 30 (`pad`), which widen the residual the
|
|
136
|
+
layout is routed through; the kNN graph and the ordinal term stay on the input. A shift-only
|
|
137
|
+
branch on fixed random frequencies (`fine_eps`, on for every input) joins the couplings that
|
|
138
|
+
write the layout halfway through training. `advanced=dict(pad=0, fine_eps=0)` turns both off.
|
|
139
|
+
|
|
140
|
+
On 250,000 cells of the HNOCA scPoli latent (10 dimensions) recall@15 is 0.134, against
|
|
141
|
+
0.102 for UMAP (default parameters, PCA initialisation) and 0.044 for the earlier
|
|
142
|
+
recipe, with both certificates passing.
|
|
143
|
+
On 103,000 bone marrow cells (PCA-50, no padding) it goes from 0.017 to 0.076. The price is
|
|
144
|
+
time: about 6 minutes at 250,000 cells on an RTX 4070 Ti, against under one minute. Every number
|
|
145
|
+
in the paper was produced with the earlier recipe (800 steps at 0.02, 12 negatives, no padding).
|
|
146
|
+
|
|
147
|
+
On CUDA the loss of each step is compiled (`compile_edges`) and its hidden layers run in fp16
|
|
148
|
+
with loss scaling (`half_edges="fp16"`), the output layers and the Fourier phase in fp32.
|
|
149
|
+
Together they are 2.2x faster than the eager fp32 path at 250,000 and 103,000 cells, at the same
|
|
150
|
+
recall and certificates, but no longer bitwise reproducible across runs;
|
|
151
|
+
`advanced=dict(compile_edges=False, half_edges="")` restores the eager path. `half_edges="bf16"`
|
|
152
|
+
is no faster and loses 2.5 to 3.5% recall.
|
|
153
|
+
|
|
154
|
+
For new points prefer `transform_opt`. Fit on 200,000 HNOCA cells, 50,000 held-out cells
|
|
155
|
+
reach recall@15 0.113 with `transform_opt` in about 6 s, against 0.031 with `transform` and
|
|
156
|
+
0.061 for the earlier recipe's `transform_opt`.
|
|
157
|
+
|
|
158
|
+
`verbose=False` turns the tqdm bar off outright. Left on, it draws only when stderr is a
|
|
159
|
+
terminal, so piping to a log does not fill it with carriage returns.
|
|
160
|
+
|
|
161
|
+
Low-level pieces are exported for custom training: `Flow`, `Coupling`, `TrainConfig`,
|
|
162
|
+
`train_flodr`, `raw_recipe`, plus the data helpers `preprocess`, `raw_coords`,
|
|
163
|
+
`fuzzy_knn_graph`, `knn_search`, `global_pairs`, `find_ab`.
|
|
164
|
+
|
|
165
|
+
## Example notebook
|
|
166
|
+
|
|
167
|
+
`examples/quickstart.ipynb` runs every call above on Fashion-MNIST, 40000 images reduced to
|
|
168
|
+
a standardised PCA-50, and takes about four minutes on a GPU. It fits on four fifths of the
|
|
169
|
+
images, embeds the rest with both transforms, sweeps `inverse_transform` over a grid of
|
|
170
|
+
screen positions to decode the layout back into pictures, and ends on the two diagnostics
|
|
171
|
+
with their certificates, both of which pass at that sample size.
|
|
172
|
+
|
|
173
|
+
## Reproducing the paper
|
|
174
|
+
|
|
175
|
+
`scripts/regen` rebuilds every figure, table and number of the paper from the released library,
|
|
176
|
+
starting from the public data sets. See `scripts/regen/README.md`.
|