AlienChen commited on
Commit
7f316fe
·
verified ·
1 Parent(s): 1d3cf07

Upload 83 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +213 -0
  2. configs/config_gfp.yaml +75 -0
  3. configs/config_selfies.yaml +57 -0
  4. data/.DS_Store +0 -0
  5. data/selfies/.DS_Store +0 -0
  6. data/selfies/28k_mimetics/dataset_dict.json +1 -0
  7. data/selfies/28k_mimetics/test/data-00000-of-00001.arrow +3 -0
  8. data/selfies/28k_mimetics/test/dataset_info.json +36 -0
  9. data/selfies/28k_mimetics/test/state.json +13 -0
  10. data/selfies/28k_mimetics/tokenizer/tokenizer_config.json +10 -0
  11. data/selfies/28k_mimetics/tokenizer/vocab.json +46 -0
  12. data/selfies/28k_mimetics/train/data-00000-of-00001.arrow +3 -0
  13. data/selfies/28k_mimetics/train/dataset_info.json +36 -0
  14. data/selfies/28k_mimetics/train/state.json +13 -0
  15. data/selfies/28k_mimetics/validation/data-00000-of-00001.arrow +3 -0
  16. data/selfies/28k_mimetics/validation/dataset_info.json +36 -0
  17. data/selfies/28k_mimetics/validation/state.json +13 -0
  18. evaluate.py +39 -0
  19. flow_matching/.DS_Store +0 -0
  20. flow_matching/__init__.py +7 -0
  21. flow_matching/loss/.DS_Store +0 -0
  22. flow_matching/loss/__init__.py +14 -0
  23. flow_matching/loss/editflows_loss.py +320 -0
  24. flow_matching/loss/generalized_loss.py +80 -0
  25. flow_matching/path/.DS_Store +0 -0
  26. flow_matching/path/__init__.py +24 -0
  27. flow_matching/path/affine.py +260 -0
  28. flow_matching/path/editflows_adapter.py +85 -0
  29. flow_matching/path/geodesic.py +100 -0
  30. flow_matching/path/mixture.py +117 -0
  31. flow_matching/path/path.py +64 -0
  32. flow_matching/path/path_sample.py +53 -0
  33. flow_matching/path/scheduler/.DS_Store +0 -0
  34. flow_matching/path/scheduler/__init__.py +29 -0
  35. flow_matching/path/scheduler/schedule_transform.py +148 -0
  36. flow_matching/path/scheduler/scheduler.py +199 -0
  37. flow_matching/solver/.DS_Store +0 -0
  38. flow_matching/solver/__init__.py +20 -0
  39. flow_matching/solver/discrete_solver.py +260 -0
  40. flow_matching/solver/editflows_solver.py +168 -0
  41. flow_matching/solver/ode_solver.py +197 -0
  42. flow_matching/solver/riemannian_ode_solver.py +261 -0
  43. flow_matching/solver/solver.py +17 -0
  44. flow_matching/solver/utils.py +19 -0
  45. flow_matching/utils/.DS_Store +0 -0
  46. flow_matching/utils/__init__.py +17 -0
  47. flow_matching/utils/categorical_sampler.py +23 -0
  48. flow_matching/utils/manifolds/.DS_Store +0 -0
  49. flow_matching/utils/manifolds/__init__.py +18 -0
  50. flow_matching/utils/manifolds/manifold.py +93 -0
README.md ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pCoMole
2
+
3
+ **pCoMole** (Pareto-Constrained Molecule editing) steers a pretrained Edit Flow toward user-specified objectives while enforcing hard terminal constraints. This repository is a minimal, clone-and-run package for:
4
+
5
+ 1. Training, evaluating, and sampling **Edit Flows**
6
+ 2. Running **pCoMole** multi-objective editing on **GFP** and **peptidomimetics**
7
+
8
+ Paper: *pCoMol: Pareto-Constrained Molecule Editing with Discrete Flows*
9
+
10
+ The original working tree is left untouched. This folder is the public-facing copy.
11
+
12
+ ## Repository layout
13
+
14
+ ```text
15
+ pCoMole/
16
+ train.py / evaluate.py # Edit Flow train / val-loss eval
17
+ configs/ # GFP (protein) and SELFIES configs
18
+ model/ logic/ flow_matching/ # Edit Flow implementation
19
+ smiles_tokenizer/ # SMILES SPE + SELFIES vocab
20
+ data/selfies/28k_mimetics/ # shipped SELFIES training split
21
+ gfp/ # GFP generation + pCoMole
22
+ pcomole.py # official GFP editor
23
+ generate.py
24
+ ckpt/last_2.ckpt
25
+ classifier_ckpt/best.pt
26
+ FPredX/ # excitation / brightness / emission models
27
+ peptidomimetics/ # peptidomimetic generation + pCoMole
28
+ pcomole.py
29
+ generate.py
30
+ objectives.py # PeptiVerse + Admetica/DeepDTAGen mix
31
+ ckpt/SELFIES_EditFlows.ckpt
32
+ ckpt/SMILES_BindEvaluator.ckpt
33
+ ckpt/admetica/ # LD50, solubility, Caco-2, half-life
34
+ ckpt/deepdtagen/
35
+ ```
36
+
37
+ ## 1. Environment
38
+
39
+ A CUDA GPU is strongly recommended.
40
+
41
+ ```bash
42
+ git clone <this-repo-url> pCoMole
43
+ cd pCoMole
44
+ python -m venv .venv
45
+ source .venv/bin/activate
46
+ pip install -r requirements.txt
47
+ ```
48
+
49
+ HuggingFace model downloads happen on first use (`facebook/esm2_t33_650M_UR50D`, `aaronfeller/PeptideCLM-23M-all`, ChemBERTa).
50
+
51
+ ### PeptiVerse (required for peptidomimetic pCoMole)
52
+
53
+ Peptidomimetic property oracles use the latest [PeptiVerse](https://huggingface.co/ChatterjeeLab/PeptiVerse) SMILES predictors. Clone it **next to** this repo, or set `PEPTIVERSE_ROOT`:
54
+
55
+ ```bash
56
+ # recommended: sibling of pCoMole
57
+ cd ..
58
+ git clone https://huggingface.co/ChatterjeeLab/PeptiVerse
59
+ cd PeptiVerse
60
+ pip install -r requirements.txt
61
+ # follow PeptiVerse README to download model weights
62
+ ```
63
+
64
+ pCoMole looks for PeptiVerse in this order:
65
+
66
+ 1. `$PEPTIVERSE_ROOT`
67
+ 2. `../PeptiVerse` relative to this repository
68
+
69
+ PeptiVerse scores the peptide-like side of each candidate. Admetica + DeepDTAGen still score the small-molecule side, and the original peptide-likeness mix logic is unchanged.
70
+
71
+ ### MAFFT (required for GFP pCoMole)
72
+
73
+ GFP excitation, brightness, and emission oracles run through FPredX and need [MAFFT](https://mafft.cbrc.jp/alignment/software/) on your `PATH`.
74
+
75
+ ```bash
76
+ # conda
77
+ conda install -c bioconda mafft
78
+
79
+ # or point to a local binary
80
+ export MAFFT_PATH=/path/to/mafft
81
+ ```
82
+
83
+ Confirm with `mafft --version` before running GFP editing.
84
+
85
+ ## 2. Train, evaluate, and sample Edit Flows
86
+
87
+ Run every command from the **repository root**.
88
+
89
+ ### SELFIES peptidomimetic Edit Flow
90
+
91
+ Training data is shipped at `data/selfies/28k_mimetics`.
92
+
93
+ ```bash
94
+ # train
95
+ python train.py --config configs/config_selfies.yaml
96
+ # optional: python train.py --config configs/config_selfies.yaml --wandb
97
+
98
+ # evaluate a checkpoint on the validation split
99
+ python evaluate.py \
100
+ --config configs/config_selfies.yaml \
101
+ --ckpt peptidomimetics/ckpt/SELFIES_EditFlows.ckpt
102
+
103
+ # unconditional / seeded generation
104
+ python peptidomimetics/generate.py \
105
+ --config configs/config_selfies.yaml \
106
+ --ckpt peptidomimetics/ckpt/SELFIES_EditFlows.ckpt \
107
+ --input 'CSCC[C@H](NC(=O)[C@@H]1CCCN1C(=O)[C@H](Cc1ccc(O)cc1)NC(=O)[C@H](CCCNC(=N)N)NC(=O)[C@H](CO)NC(=O)[C@H](Cc1ccc(O)cc1)NC(=O)[C@@H]1CCCN1C(=O)[C@@H]1CCCN1C(=O)[C@@H]1CCCN1C(=O)[C@@H]1CCCN1C(=O)[C@@H](N)CO)C(=O)N[C@@H](CC(=O)O)C(=O)O' \
108
+ --num_steps 30 \
109
+ --num_samples 2 \
110
+ --output_csv outputs/peptidomimetic_unconditional.csv
111
+ ```
112
+
113
+ Or: `bash peptidomimetics/scripts/train.sh` and `bash peptidomimetics/scripts/generate.sh`.
114
+
115
+ ### GFP protein Edit Flow
116
+
117
+ A trained GFP checkpoint is shipped at `gfp/ckpt/last_2.ckpt`. The original GFP training arrows are **not** included. To retrain, put a HuggingFace `load_from_disk` dataset under `data/gfp/{train,validation}` (or edit `configs/config_gfp.yaml`).
118
+
119
+ ```bash
120
+ python evaluate.py --config configs/config_gfp.yaml --ckpt gfp/ckpt/last_2.ckpt
121
+
122
+ python gfp/generate.py \
123
+ --config configs/config_gfp.yaml \
124
+ --ckpt gfp/ckpt/last_2.ckpt \
125
+ --input 'MSSGALLFHGKIPYVVEMEGNVDGHTFSIRGKGYGDASVGKVDAQFICTTGDVPVPWSTLVTTLTYGAQCFAKYGPELKDFYKSCMPDGYVQERTITFEGDGNFKTRAEVTFENGSVYNRVKLNGQGFKKDGHVLGKNLEFNFTPHCLYIWGDQANHGLKSAFKICHEITGSKGDFIVADHTQMNTPIGGGPVHVPEYHHMSYHVKLSKDVTDHRDNMSLKETVRAVDCRKTYDFDAGSGDTS' \
126
+ --num_steps 10 \
127
+ --num_samples 8 \
128
+ --output_csv outputs/gfp_unconditional.csv
129
+ ```
130
+
131
+ ## 3. pCoMole multi-objective editing
132
+
133
+ ### GFP
134
+
135
+ Official editor: `gfp/pcomole.py` (length + excitation + brightness, with GFP-classifier and emission constraints).
136
+
137
+ ```bash
138
+ bash gfp/scripts/pcomole.sh
139
+ ```
140
+
141
+ Equivalent command:
142
+
143
+ ```bash
144
+ python gfp/pcomole.py \
145
+ --root_dir gfp/FPredX \
146
+ --config configs/config_gfp.yaml \
147
+ --ckpt gfp/ckpt/last_2.ckpt \
148
+ --num_steps 10 \
149
+ --num_candidates 50 \
150
+ --num_rollouts 10 \
151
+ --objective_weights 3 1 1 \
152
+ --output_file outputs/gfp_length_excitation_brightness.csv \
153
+ --input 'MSSGALLFHGKIPYVVEMEGNVDGHTFSIRGKGYGDASVGKVDAQFICTTGDVPVPWSTLVTTLTYGAQCFAKYGPELKDFYKSCMPDGYVQERTITFEGDGNFKTRAEVTFENGSVYNRVKLNGQGFKKDGHVLGKNLEFNFTPHCLYIWGDQANHGLKSAFKICHEITGSKGDFIVADHTQMNTPIGGGPVHVPEYHHMSYHVKLSKDVTDHRDNMSLKETVRAVDCRKTYDFDAGSGDTS'
154
+ ```
155
+
156
+ If the output filename contains `length_excitation_brightness`, `length_brightness`, `length_excitation`, or `length`, the matching objective subset is used. Any other name uses all three objectives.
157
+
158
+ ### Peptidomimetics
159
+
160
+ Official editor: `peptidomimetics/pcomole.py`.
161
+
162
+ Objectives (7 scores when `--specificity` is on):
163
+
164
+ 1. non-toxicity
165
+ 2. solubility
166
+ 3. permeability
167
+ 4. half-life
168
+ 5. affinity
169
+ 6. motif
170
+ 7. specificity
171
+
172
+ PeptiVerse provides the peptide-side scores. Admetica + DeepDTAGen provide the small-molecule side. The two are mixed by peptide-likeness, same as the paper code. Motif / specificity still use the shipped BindEvaluator checkpoint.
173
+
174
+ ```bash
175
+ # after PeptiVerse is cloned and its weights are downloaded
176
+ bash peptidomimetics/scripts/pcomole.sh
177
+ ```
178
+
179
+ `--target` and `--motifs` are required for affinity and motif oracles. `--specificity` adds the seventh objective.
180
+
181
+ ## 4. Included checkpoints
182
+
183
+ | Path | Role |
184
+ |---|---|
185
+ | `gfp/ckpt/last_2.ckpt` | GFP Edit Flow |
186
+ | `gfp/classifier_ckpt/best.pt` | GFP hard constraint |
187
+ | `gfp/FPredX/{ex,bright,em}_model` | FPredX property models |
188
+ | `peptidomimetics/ckpt/SELFIES_EditFlows.ckpt` | peptidomimetic Edit Flow |
189
+ | `peptidomimetics/ckpt/SMILES_BindEvaluator.ckpt` | motif / specificity |
190
+ | `peptidomimetics/ckpt/admetica/*.ckpt` | Admetica blending oracles |
191
+ | `peptidomimetics/ckpt/deepdtagen/` | DeepDTAGen affinity |
192
+
193
+ Together these are about 10 GB. HuggingFace uploads should use Git LFS for the `.ckpt` / `.pt` / `.pth` files.
194
+
195
+ ## 5. Environment variables
196
+
197
+ | Variable | Purpose |
198
+ |---|---|
199
+ | `PEPTIVERSE_ROOT` | PeptiVerse repo root (manifest + `training_classifiers/`) |
200
+ | `MAFFT_PATH` | MAFFT binary, if it is not on `PATH` |
201
+ | `GFP_CLASSIFIER_CKPT` | optional override for the GFP classifier |
202
+
203
+ ## 6. Notes
204
+
205
+ - Run scripts from the repo root, or use the wrappers under `*/scripts/`.
206
+ - GFP FPredX will fail immediately if `mafft` is missing; that is expected.
207
+ - Peptidomimetic pCoMole will fail at oracle init if PeptiVerse is missing or its weights were not downloaded.
208
+ - Cas9 is not part of this release.
209
+ - Training writes to `outputs/` by default.
210
+
211
+ ## Citation
212
+
213
+ If you use this code, please cite the pCoMole paper and [PeptiVerse](https://www.biorxiv.org/content/10.64898/2025.12.31.697180).
configs/config_gfp.yaml ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ task: protein
2
+
3
+ compute:
4
+ ngpus: 1
5
+ nodes: 1
6
+
7
+ logging:
8
+ enable_wandb: False
9
+ project: pCoMole
10
+
11
+ data:
12
+ # GFP training arrows are not shipped. Point these at your own
13
+ # HuggingFace load_from_disk() dataset if you want to retrain.
14
+ train_path: data/gfp/train
15
+ val_path: data/gfp/validation
16
+ num_workers: 1
17
+
18
+ training:
19
+ batch_size: None
20
+ snapshot: 100
21
+ eval_freq: 1000
22
+ perplexity_freq: 100
23
+ seed: 42
24
+ grad_accumulation_steps: 2
25
+ loc_prop_path: False
26
+ lam_prop: 1
27
+ reparameterize: True
28
+ gamma_rate: 1
29
+ gamma_edit: 1
30
+ use_aux_ce: False
31
+ aux_ce_weight: 1.0
32
+ no_training: False
33
+
34
+ eval:
35
+ batch_size: None
36
+ sample_batch_size: 4
37
+ perplexity: False
38
+ perplexity_batch_size: 4
39
+
40
+ optim:
41
+ weight_decay: 0.05
42
+ optimizer: AdamW
43
+ lr: 1.0e-6
44
+ beta1: 0.9
45
+ beta2: 0.95
46
+ eps: 1.0e-8
47
+ grad_clip: 1.0
48
+ eta_min_ratio: 0.1
49
+ fused: false
50
+ n_epochs: 1000
51
+ warmup_ratio: 0.1
52
+
53
+ flow:
54
+ source_distribution: uniform
55
+ loss_function: editflows
56
+ exponent: 2.0
57
+ scheduler_type: polynomial
58
+ sampling_steps: 256
59
+ eps_id: -1
60
+
61
+ model:
62
+ d_model: 768
63
+ max_len: 2048
64
+ n_layers: 12
65
+ n_heads: 12
66
+ dropout: 0.1
67
+ attn_dropout: 0.0
68
+ proj_dropout: 0.0
69
+ compile: False
70
+ esm_model_name: facebook/esm2_t33_650M_UR50D
71
+ freeze_esm: true
72
+ p_optimal: 0
73
+ scale_size: 0.5
74
+
75
+ work_dir: outputs/gfp
configs/config_selfies.yaml ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ task: selfies
2
+
3
+ compute:
4
+ ngpus: 1
5
+ nodes: 1
6
+
7
+ data:
8
+ train_path: data/selfies/28k_mimetics/train
9
+ val_path: data/selfies/28k_mimetics/validation
10
+ num_workers: 4
11
+
12
+ training:
13
+ batch_size: 4
14
+ snapshot: 100
15
+ eval_freq: 1000
16
+ perplexity_freq: 100
17
+ seed: 42
18
+
19
+ eval:
20
+ batch_size: 4
21
+ sample_batch_size: 4
22
+ perplexity: False
23
+ perplexity_batch_size: 4
24
+
25
+ optim:
26
+ weight_decay: 0.03
27
+ optimizer: AdamW
28
+ lr: 3.0e-4
29
+ beta1: 0.9
30
+ beta2: 0.95
31
+ eps: 1.0e-8
32
+ grad_clip: 1.0
33
+ eta_min_ratio: 0.1
34
+ fused: false
35
+ n_epochs: 100
36
+ warmup_ratio: 0.1
37
+
38
+ flow:
39
+ source_distribution: uniform
40
+ loss_function: editflows_reparam
41
+ exponent: 2.0
42
+ scheduler_type: polynomial
43
+ sampling_steps: 256
44
+ eps_id: -1
45
+
46
+ model:
47
+ hidden_size: 768
48
+ cond_dim: 128
49
+ length: 2048
50
+ n_blocks: 8
51
+ n_heads: 12
52
+ dropout: 0.1
53
+ compile: False
54
+ p_optimal: 0
55
+ scale_size: 1.0
56
+
57
+ work_dir: outputs/selfies
data/.DS_Store ADDED
Binary file (8.2 kB). View file
 
data/selfies/.DS_Store ADDED
Binary file (8.2 kB). View file
 
data/selfies/28k_mimetics/dataset_dict.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"splits": ["train", "validation", "test"]}
data/selfies/28k_mimetics/test/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d709f3386113ebc6abe9400950274e2e41aaf7063b31e67d6ba15f3239462251
3
+ size 786928
data/selfies/28k_mimetics/test/dataset_info.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "input_ids": {
6
+ "feature": {
7
+ "feature": {
8
+ "dtype": "int32",
9
+ "_type": "Value"
10
+ },
11
+ "_type": "List"
12
+ },
13
+ "_type": "List"
14
+ },
15
+ "attention_mask": {
16
+ "feature": {
17
+ "feature": {
18
+ "dtype": "int8",
19
+ "_type": "Value"
20
+ },
21
+ "_type": "List"
22
+ },
23
+ "_type": "List"
24
+ },
25
+ "seq_length": {
26
+ "dtype": "int64",
27
+ "_type": "Value"
28
+ },
29
+ "batch_size": {
30
+ "dtype": "int64",
31
+ "_type": "Value"
32
+ }
33
+ },
34
+ "homepage": "",
35
+ "license": ""
36
+ }
data/selfies/28k_mimetics/test/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "98db6912c1e59254",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
data/selfies/28k_mimetics/tokenizer/tokenizer_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "type": "selfies-wordlevel",
3
+ "version": "2.1.1",
4
+ "special_tokens": {
5
+ "pad": "<pad>",
6
+ "bos": "<bos>",
7
+ "eos": "<eos>"
8
+ },
9
+ "semantic_constraints": null
10
+ }
data/selfies/28k_mimetics/tokenizer/vocab.json ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "<bos>": 1,
3
+ "<eos>": 2,
4
+ "<pad>": 0,
5
+ "[#Branch1]": 3,
6
+ "[#Branch2]": 4,
7
+ "[#C]": 5,
8
+ "[#N]": 6,
9
+ "[/-Ring1]": 7,
10
+ "[/C]": 8,
11
+ "[/N]": 9,
12
+ "[=Branch1]": 10,
13
+ "[=Branch2]": 11,
14
+ "[=C]": 12,
15
+ "[=N]": 13,
16
+ "[=O]": 14,
17
+ "[=Ring1]": 15,
18
+ "[=Ring2]": 16,
19
+ "[=S]": 17,
20
+ "[Br]": 18,
21
+ "[Branch1]": 19,
22
+ "[Branch2]": 20,
23
+ "[C@@H1]": 21,
24
+ "[C@@]": 22,
25
+ "[C@H1]": 23,
26
+ "[C@]": 24,
27
+ "[CH1-1]": 25,
28
+ "[C]": 26,
29
+ "[Cl]": 27,
30
+ "[F]": 28,
31
+ "[I]": 29,
32
+ "[N+1]": 30,
33
+ "[N-1]": 31,
34
+ "[NH1]": 32,
35
+ "[N]": 33,
36
+ "[O-1]": 34,
37
+ "[O]": 35,
38
+ "[P]": 36,
39
+ "[Ring1]": 37,
40
+ "[Ring2]": 38,
41
+ "[S]": 39,
42
+ "[\\-Ring1]": 40,
43
+ "[\\-Ring2]": 41,
44
+ "[\\C]": 42,
45
+ "[\\N]": 43
46
+ }
data/selfies/28k_mimetics/train/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4d7e3d408b36d7c8a75b0d36f20d09b744c1c845961401c72a4023a9a6ffa3f6
3
+ size 6084456
data/selfies/28k_mimetics/train/dataset_info.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "input_ids": {
6
+ "feature": {
7
+ "feature": {
8
+ "dtype": "int32",
9
+ "_type": "Value"
10
+ },
11
+ "_type": "List"
12
+ },
13
+ "_type": "List"
14
+ },
15
+ "attention_mask": {
16
+ "feature": {
17
+ "feature": {
18
+ "dtype": "int8",
19
+ "_type": "Value"
20
+ },
21
+ "_type": "List"
22
+ },
23
+ "_type": "List"
24
+ },
25
+ "seq_length": {
26
+ "dtype": "int64",
27
+ "_type": "Value"
28
+ },
29
+ "batch_size": {
30
+ "dtype": "int64",
31
+ "_type": "Value"
32
+ }
33
+ },
34
+ "homepage": "",
35
+ "license": ""
36
+ }
data/selfies/28k_mimetics/train/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "58b5e20848e4941d",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
data/selfies/28k_mimetics/validation/data-00000-of-00001.arrow ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6667b7a12a440b50d996c8523813df0965a718e1a467ef2162fdea65d9b972cb
3
+ size 778640
data/selfies/28k_mimetics/validation/dataset_info.json ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "citation": "",
3
+ "description": "",
4
+ "features": {
5
+ "input_ids": {
6
+ "feature": {
7
+ "feature": {
8
+ "dtype": "int32",
9
+ "_type": "Value"
10
+ },
11
+ "_type": "List"
12
+ },
13
+ "_type": "List"
14
+ },
15
+ "attention_mask": {
16
+ "feature": {
17
+ "feature": {
18
+ "dtype": "int8",
19
+ "_type": "Value"
20
+ },
21
+ "_type": "List"
22
+ },
23
+ "_type": "List"
24
+ },
25
+ "seq_length": {
26
+ "dtype": "int64",
27
+ "_type": "Value"
28
+ },
29
+ "batch_size": {
30
+ "dtype": "int64",
31
+ "_type": "Value"
32
+ }
33
+ },
34
+ "homepage": "",
35
+ "license": ""
36
+ }
data/selfies/28k_mimetics/validation/state.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_data_files": [
3
+ {
4
+ "filename": "data-00000-of-00001.arrow"
5
+ }
6
+ ],
7
+ "_fingerprint": "8dc499c569440f6b",
8
+ "_format_columns": null,
9
+ "_format_kwargs": {},
10
+ "_format_type": null,
11
+ "_output_all_columns": false,
12
+ "_split": null
13
+ }
evaluate.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+
3
+ import torch
4
+ import pytorch_lightning as pl
5
+
6
+ from paths import add_repo_to_sys_path, resolve_path
7
+ from train import build_dataloaders, build_editflow, load_config
8
+
9
+ add_repo_to_sys_path()
10
+
11
+
12
+ def main():
13
+ parser = argparse.ArgumentParser(description="Evaluate an Edit Flow checkpoint on the validation split")
14
+ parser.add_argument("--config", type=str, required=True)
15
+ parser.add_argument("--ckpt", type=str, required=True)
16
+ args = parser.parse_args()
17
+
18
+ cfg = load_config(args.config)
19
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
20
+ editflow, _, _, _, _, _, _ = build_editflow(cfg, device=device)
21
+
22
+ ckpt = torch.load(resolve_path(args.ckpt), map_location=device, weights_only=False)
23
+ state = ckpt["state_dict"] if isinstance(ckpt, dict) and "state_dict" in ckpt else ckpt
24
+ editflow.load_state_dict(state, strict=False)
25
+ editflow.eval()
26
+
27
+ _, val_dataloader = build_dataloaders(cfg)
28
+ trainer = pl.Trainer(
29
+ accelerator="gpu" if torch.cuda.is_available() else "cpu",
30
+ devices=1,
31
+ logger=False,
32
+ enable_checkpointing=False,
33
+ )
34
+ metrics = trainer.validate(editflow, val_dataloader, verbose=True)
35
+ print(metrics)
36
+
37
+
38
+ if __name__ == "__main__":
39
+ main()
flow_matching/.DS_Store ADDED
Binary file (14.3 kB). View file
 
flow_matching/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ __version__ = "1.0.10"
flow_matching/loss/.DS_Store ADDED
Binary file (6.15 kB). View file
 
flow_matching/loss/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from .generalized_loss import MixturePathGeneralizedKL
8
+ from .editflows_loss import EditFlowsLoss, EditFlowsLossReParam
9
+
10
+ __all__ = [
11
+ "MixturePathGeneralizedKL",
12
+ "EditFlowsLoss",
13
+ "EditFlowsLossReParam"
14
+ ]
flow_matching/loss/editflows_loss.py ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc.
2
+ # All rights reserved.
3
+
4
+ from typing import List, Optional, Sequence
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torch import Tensor
8
+ from torch.nn.modules.loss import _Loss
9
+
10
+ # If you kept the adapter around only for scheduler access you can drop it entirely,
11
+ # because we pass precomputed_weight from training.step. Keeping it is harmless though.
12
+ class EditFlowsLoss(_Loss):
13
+ """
14
+ Edit Flows loss (Eq. 23), ragged version:
15
+ L_i = (sum_j λ_ins[i][j] + sum_j λ_del[i][j] + sum_j λ_sub[i][j])
16
+ - w_i * ( sum_{ins events e} [log λ_ins[i][slot_e] + log Q_ins[i](y_e)]
17
+ + sum_{del j} [log λ_del[i][j]]
18
+ + sum_{sub j} [log λ_sub[i][j] + log Q_sub[i](y_j)] )
19
+ """
20
+
21
+ def __init__(self, reduction: str = "mean") -> None:
22
+ super().__init__(None, None, reduction)
23
+
24
+ def forward(
25
+ self,
26
+ lam_ins: torch.Tensor, # (B, L)
27
+ logits_ins: torch.Tensor, # (B, L, V)
28
+ lam_del: torch.Tensor, # (B, L)
29
+ lam_sub: torch.Tensor, # (B, L)
30
+ logits_sub: torch.Tensor, # (B, L, V)
31
+ z_t: torch.Tensor, # (B, N) aligned, with eps_id
32
+ z_1: torch.Tensor, # (B, N) aligned target, with eps_id
33
+ x_t: torch.Tensor, # (B, L)
34
+ valid_mask: torch.Tensor, # (B, L) bool, False==padding, True==valid
35
+ precomputed_weight: torch.Tensor, # (B,) or () = kappa_dot/(1-kappa)
36
+ eps_id: int,
37
+ bos_id: int,
38
+ eos_id: int,
39
+ ) -> torch.Tensor:
40
+ """
41
+ Implements Eq. 23 style loss for Edit Flows.
42
+
43
+ We:
44
+ 1) penalize total outgoing rate on x_t
45
+ 2) for every column in (z_t, z_1) that still differs, we map it to an x_t edit
46
+ and add - w * log u_required
47
+
48
+ BOS/EOS handling:
49
+ - z_t and z_1 are aligned and already contain BOS/EOS
50
+ - we do NOT allow edits that delete/replace BOS/EOS
51
+ - we also skip columns whose target token is BOS/EOS (they should already match)
52
+ """
53
+ device = lam_ins.device
54
+ B, L = x_t.shape
55
+ _, N = z_t.shape
56
+
57
+ # 1. ----- RATE TERM -----
58
+ # valid_mask: True = real token, False = pad
59
+ valid_f = valid_mask.to(lam_ins.dtype) # (B, L) 1.0 for valid, 0.0 for pad
60
+
61
+ # total outgoing rate at each position
62
+ total_rate_pos = lam_ins + lam_del + lam_sub # (B, L)
63
+ total_rate_pos = total_rate_pos * valid_f # zero out pads
64
+ loss_rate = total_rate_pos.sum(dim=1) # (B,)
65
+
66
+ # 2. ----- EDIT TERM -----
67
+ # precompute log-softmax for tokens (better numerics)
68
+ logp_ins = F.log_softmax(logits_ins, dim=-1) # (B, L, V)
69
+ logp_sub = F.log_softmax(logits_sub, dim=-1) # (B, L, V)
70
+
71
+ # Make weight shape nice
72
+ if precomputed_weight.dim() == 0:
73
+ precomputed_weight = precomputed_weight.view(1).expand(B).to(device)
74
+ else:
75
+ precomputed_weight = precomputed_weight.to(device)
76
+
77
+ # use float for accumulation
78
+ loss_edit = torch.zeros(B, dtype=torch.float32, device=device)
79
+
80
+ for b in range(B):
81
+ # how many *valid* tokens in x_t[b]
82
+ valid_len = int(valid_mask[b].sum().item())
83
+
84
+ prefix_non_eps = 0 # = number of non-ε seen in z_t[b, :i]
85
+
86
+ for i in range(N):
87
+ zt = int(z_t[b, i].item())
88
+ z1 = int(z_1[b, i].item())
89
+
90
+ # map this column to an x_t position
91
+ if zt != eps_id:
92
+ # this column corresponds to x_t[b, prefix_non_eps]
93
+ x_pos = prefix_non_eps
94
+ is_token = True
95
+ prefix_non_eps += 1
96
+ else:
97
+ # this is a gap column, sits BETWEEN tokens
98
+ x_pos = prefix_non_eps
99
+ is_token = False
100
+
101
+ # if already matched -> no term
102
+ if zt == z1:
103
+ continue
104
+
105
+ # --- BOS/EOS guards on the target side ---
106
+ # if the target token is BOS or EOS, we shouldn't try to force an edit here
107
+ if z1 == bos_id or z1 == eos_id:
108
+ # skip this column; aligned BOS/EOS should already match
109
+ continue
110
+
111
+ # figure out which edit we need
112
+ # CASE 1: deletion: z_t has token, z_1 has ε
113
+ if is_token and (z1 == eps_id):
114
+ # delete token at x_pos
115
+ if x_pos >= valid_len:
116
+ # out of range (shouldn't happen if alignment & mask match)
117
+ raise NotImplementedError
118
+
119
+ x_token = int(x_t[b, x_pos].item())
120
+ # do NOT delete BOS/EOS
121
+ if x_token == bos_id or x_token == eos_id:
122
+ continue
123
+
124
+ lam = lam_del[b, x_pos].clamp_min(1e-12)
125
+ log_u_req = torch.log(lam)
126
+
127
+ # CASE 2: substitution: token -> different token
128
+ elif is_token and (z1 != eps_id) and (zt != z1):
129
+ if x_pos >= valid_len:
130
+ raise NotImplementedError
131
+
132
+ x_token = int(x_t[b, x_pos].item())
133
+ # do NOT substitute BOS/EOS
134
+ if x_token == bos_id or x_token == eos_id:
135
+ continue
136
+
137
+ lam = lam_sub[b, x_pos].clamp_min(1e-12)
138
+ logp_tok = logp_sub[b, x_pos, z1]
139
+ log_u_req = torch.log(lam) + logp_tok
140
+
141
+ # CASE 3: insertion: ε -> token
142
+ elif (not is_token) and (z1 != eps_id):
143
+ # insertion in the gap after token (x_pos - 1)
144
+ # if x_pos == 0, we insert after "BOS"/at start -> map to position 0
145
+ ins_pos = x_pos - 1
146
+ if ins_pos < 0:
147
+ ins_pos = 0
148
+
149
+ # clamp to last valid position if needed
150
+ if valid_len == 0:
151
+ # degenerate, but avoid -1
152
+ ins_pos = 0
153
+ elif ins_pos >= valid_len:
154
+ ins_pos = valid_len - 1
155
+
156
+ # also don't insert "after" EOS if ins_pos currently points to EOS
157
+ x_token = int(x_t[b, ins_pos].item())
158
+ if x_token == eos_id:
159
+ # simplest policy: skip this insertion supervision
160
+ continue
161
+
162
+ lam = lam_ins[b, ins_pos].clamp_min(1e-12)
163
+ logp_tok = logp_ins[b, ins_pos, z1]
164
+ log_u_req = torch.log(lam) + logp_tok
165
+
166
+ else:
167
+ # unknown pattern (shouldn't happen with proper alignment)
168
+ raise NotImplementedError
169
+
170
+ w = precomputed_weight[b]
171
+ loss_edit[b] += - w * log_u_req
172
+
173
+ # 3. ----- COMBINE -----
174
+ loss = loss_rate + loss_edit # (B,)
175
+ loss = loss.mean()
176
+ return loss
177
+
178
+
179
+ class EditFlowsLossReParam(_Loss):
180
+ """
181
+ Edit Flows loss (Eq. 23) but parameterized by:
182
+ - lam_total: (B, L) >= 0
183
+ - pi_type: (B, L, 3) prob over {ins, del, sub}, sums to 1
184
+
185
+ Token distributions are still given by logits_ins/logits_sub.
186
+
187
+ Required-edit log rate:
188
+ del: log u = log lam_total[pos] + log pi_del[pos]
189
+ sub: log u = log lam_total[pos] + log pi_sub[pos] + log Q_sub(token)
190
+ ins: log u = log lam_total[pos] + log pi_ins[pos] + log Q_ins(token)
191
+ """
192
+
193
+ def __init__(self, reduction: str = "mean", gamma_rate: float = 1.0, eps: float = 1e-12) -> None:
194
+ super().__init__(None, None, reduction)
195
+ self.gamma_rate = float(gamma_rate)
196
+ self.eps = float(eps)
197
+
198
+ def forward(
199
+ self,
200
+ lam_total: torch.Tensor, # (B, L)
201
+ pi_type: torch.Tensor, # (B, L, 3) probabilities over {ins, del, sub}
202
+ logits_ins: torch.Tensor, # (B, L, V)
203
+ logits_sub: torch.Tensor, # (B, L, V)
204
+ z_t: torch.Tensor, # (B, N) aligned, with eps_id
205
+ z_1: torch.Tensor, # (B, N) aligned target, with eps_id
206
+ x_t: torch.Tensor, # (B, L)
207
+ valid_mask: torch.Tensor, # (B, L) bool, False==padding, True==valid (same as your old loss)
208
+ precomputed_weight: torch.Tensor, # (B,) or () = kappa_dot/(1-kappa)
209
+ eps_id: int,
210
+ bos_id: int,
211
+ eos_id: int,
212
+ ) -> torch.Tensor:
213
+ device = lam_total.device
214
+ B, L = x_t.shape
215
+ _, N = z_t.shape
216
+
217
+ # --- RATE TERM ---
218
+ valid_f = valid_mask.to(lam_total.dtype) # True=valid -> 1.0
219
+ loss_rate = (lam_total * valid_f).sum(dim=1) # (B,)
220
+ loss_rate = self.gamma_rate * loss_rate
221
+
222
+ # --- EDIT TERM ---
223
+ logp_ins = F.log_softmax(logits_ins, dim=-1) # (B, L, V)
224
+ logp_sub = F.log_softmax(logits_sub, dim=-1) # (B, L, V)
225
+
226
+ # normalize pi_type defensively (in case caller passes slightly off-simplex)
227
+ pi_sum = pi_type.sum(dim=-1, keepdim=True).clamp_min(self.eps)
228
+ pi_type = pi_type / pi_sum
229
+
230
+ log_lam = torch.log(lam_total.clamp_min(self.eps)) # (B, L)
231
+ log_pi = torch.log(pi_type.clamp_min(self.eps)) # (B, L, 3)
232
+
233
+ # weight shape
234
+ if precomputed_weight.dim() == 0:
235
+ precomputed_weight = precomputed_weight.view(1).expand(B).to(device)
236
+ else:
237
+ precomputed_weight = precomputed_weight.to(device)
238
+
239
+ loss_edit = torch.zeros(B, dtype=torch.float32, device=device)
240
+
241
+ for b in range(B):
242
+ valid_len = int(valid_mask[b].sum().item())
243
+ prefix_non_eps = 0
244
+
245
+ for i in range(N):
246
+ zt = int(z_t[b, i].item())
247
+ z1 = int(z_1[b, i].item())
248
+
249
+ # map this alignment column to an x_t position
250
+ if zt != eps_id:
251
+ x_pos = prefix_non_eps
252
+ is_token = True
253
+ prefix_non_eps += 1
254
+ else:
255
+ x_pos = prefix_non_eps
256
+ is_token = False
257
+
258
+ # already matched -> no supervision
259
+ if zt == z1:
260
+ continue
261
+
262
+ # don't force edits where target is BOS/EOS
263
+ if z1 == bos_id or z1 == eos_id:
264
+ continue
265
+
266
+ # CASE 1: deletion (token -> eps)
267
+ if is_token and (z1 == eps_id):
268
+ if x_pos >= valid_len:
269
+ raise NotImplementedError
270
+
271
+ x_token = int(x_t[b, x_pos].item())
272
+ if x_token == bos_id or x_token == eos_id:
273
+ continue
274
+
275
+ log_u_req = log_lam[b, x_pos] + log_pi[b, x_pos, 1] # del index = 1
276
+
277
+ # CASE 2: substitution (token -> different token)
278
+ elif is_token and (z1 != eps_id) and (zt != z1):
279
+ if x_pos >= valid_len:
280
+ raise NotImplementedError
281
+
282
+ x_token = int(x_t[b, x_pos].item())
283
+ if x_token == bos_id or x_token == eos_id:
284
+ continue
285
+
286
+ log_u_req = (
287
+ log_lam[b, x_pos]
288
+ + log_pi[b, x_pos, 2] # sub index = 2
289
+ + logp_sub[b, x_pos, z1]
290
+ )
291
+
292
+ # CASE 3: insertion (eps -> token), map gap to "after token (x_pos-1)"
293
+ elif (not is_token) and (z1 != eps_id):
294
+ ins_pos = x_pos - 1
295
+ if ins_pos < 0:
296
+ ins_pos = 0
297
+
298
+ if valid_len == 0:
299
+ ins_pos = 0
300
+ elif ins_pos >= valid_len:
301
+ ins_pos = valid_len - 1
302
+
303
+ x_token = int(x_t[b, ins_pos].item())
304
+ if x_token == eos_id:
305
+ continue
306
+
307
+ log_u_req = (
308
+ log_lam[b, ins_pos]
309
+ + log_pi[b, ins_pos, 0] # ins index = 0
310
+ + logp_ins[b, ins_pos, z1]
311
+ )
312
+
313
+ else:
314
+ raise NotImplementedError
315
+
316
+ w = precomputed_weight[b]
317
+ loss_edit[b] += -w * log_u_req
318
+
319
+ loss = loss_rate + loss_edit
320
+ return loss.mean()
flow_matching/loss/generalized_loss.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import torch
8
+ from torch import Tensor
9
+ from torch.nn.modules.loss import _Loss
10
+
11
+ from flow_matching.path import MixtureDiscreteProbPath
12
+
13
+
14
+ class MixturePathGeneralizedKL(_Loss):
15
+ r"""A generalized KL loss for discrete flow matching.
16
+ A class that measures the generalized KL of a discrete flow model :math:`p_{1|t}` w.r.t. a probability path given by ``path``. Note: this class is assuming that the model is trained on the same path.
17
+
18
+ For a model trained on a space :math:`\mathcal{S} = \mathcal{T}^d`, :math:`\mathcal{T} = [K] = \set{1,2,\ldots,K}`, the loss is given by
19
+
20
+ .. math::
21
+ \ell_i(x_1, x_t, t) = -\frac{\dot{\kappa}_t}{1-\kappa_t} \biggr[ p_{1|t}(x_t^i|x_t) -\delta_{x^i_1}(x_t^i) + (1-\delta_{x^i_1}(x_t^i))\left(\log p_{1|t}(x_1^i|x_t)\right)\biggr],
22
+
23
+ where :math:`\kappa_t` is the scheduler associated with ``path``.
24
+
25
+ Args:
26
+ path (MixtureDiscreteProbPath): Probability path (x-prediction training).
27
+ reduction (str, optional): Specify the reduction to apply to the output ``'none'`` | ``'mean'`` | ``'sum'``. ``'none'``: no reduction is applied to the output, ``'mean'``: the output is reduced by mean over sequence elements, ``'sum'``: the output is reduced by sum over sequence elements. Defaults to 'mean'.
28
+ """
29
+
30
+ def __init__(self, path: MixtureDiscreteProbPath, reduction: str = "mean") -> None:
31
+ super().__init__(None, None, reduction)
32
+ self.path = path
33
+
34
+ def forward(self, logits: Tensor, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
35
+ r"""Evaluates the generalized KL loss.
36
+
37
+ Args:
38
+ logits (Tensor): posterior model output (i.e., softmax(``logits``) :math:`=p_{1|t}(x|x_t)`), shape (batch, d, K).
39
+ x_1 (Tensor): target data point :math:`x_1 \sim q`, shape (batch, d).
40
+ x_t (Tensor): conditional sample at :math:`x_t \sim p_t(\cdot|x_1)`, shape (batch, d).
41
+ t (Tensor): times in :math:`[0,1]`, shape (batch).
42
+
43
+ Raises:
44
+ ValueError: reduction value must be one of ``'none'`` | ``'mean'`` | ``'sum'``.
45
+
46
+ Returns:
47
+ Tensor: Generalized KL loss.
48
+ """
49
+ x_1_shape = x_1.shape
50
+
51
+ # extract x_1 value of log(p_{1|t}(x|x_t)).
52
+ log_p_1t = torch.log_softmax(logits, dim=-1)
53
+ log_p_1t_x1 = torch.gather(log_p_1t, dim=-1, index=x_1.unsqueeze(-1))
54
+ log_p_1t_x1 = log_p_1t_x1.view(*x_1_shape)
55
+
56
+ # extract x_t value of p_{1|t}(x|x_t).
57
+ p_1t = torch.exp(log_p_1t)
58
+ p_1t_xt = torch.gather(p_1t, dim=-1, index=x_t.unsqueeze(-1))
59
+ p_1t_xt = p_1t_xt.view(*x_1_shape)
60
+
61
+ scheduler_output = self.path.scheduler(t)
62
+
63
+ jump_coefficient = (
64
+ scheduler_output.d_alpha_t / (1 - scheduler_output.alpha_t)
65
+ )[(...,) + (None,) * (x_1.dim() - 1)]
66
+ jump_coefficient = jump_coefficient.repeat(1, *x_1_shape[1:])
67
+ delta_x1_xt = (x_t == x_1).to(log_p_1t.dtype)
68
+
69
+ loss = -jump_coefficient * (
70
+ p_1t_xt - delta_x1_xt + (1 - delta_x1_xt) * log_p_1t_x1
71
+ )
72
+
73
+ if self.reduction == "mean":
74
+ return torch.mean(loss)
75
+ elif self.reduction == "sum":
76
+ return torch.sum(loss)
77
+ elif self.reduction == "none":
78
+ return loss
79
+ else:
80
+ raise ValueError(f"{self.reduction} is not a valid value for reduction")
flow_matching/path/.DS_Store ADDED
Binary file (8.2 kB). View file
 
flow_matching/path/__init__.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from .affine import AffineProbPath, CondOTProbPath
8
+ from .geodesic import GeodesicProbPath
9
+ from .mixture import MixtureDiscreteProbPath
10
+ from .path import ProbPath
11
+ from .path_sample import DiscretePathSample, PathSample
12
+ from .editflows_adapter import EditFlowsPathAdapter
13
+
14
+
15
+ __all__ = [
16
+ "ProbPath",
17
+ "AffineProbPath",
18
+ "CondOTProbPath",
19
+ "MixtureDiscreteProbPath",
20
+ "GeodesicProbPath",
21
+ "PathSample",
22
+ "DiscretePathSample",
23
+ "EditFlowsPathAdapter",
24
+ ]
flow_matching/path/affine.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from torch import Tensor
8
+
9
+ from flow_matching.path.path import ProbPath
10
+ from flow_matching.path.path_sample import PathSample
11
+ from flow_matching.path.scheduler.scheduler import CondOTScheduler, Scheduler
12
+ from flow_matching.utils import expand_tensor_like
13
+
14
+
15
+ class AffineProbPath(ProbPath):
16
+ r"""The ``AffineProbPath`` class represents a specific type of probability path where the transformation between distributions is affine.
17
+ An affine transformation can be represented as:
18
+
19
+ .. math::
20
+
21
+ X_t = \alpha_t X_1 + \sigma_t X_0,
22
+
23
+ where :math:`X_t` is the transformed data point at time `t`. :math:`X_0` and :math:`X_1` are the source and target data points, respectively. :math:`\alpha_t` and :math:`\sigma_t` are the parameters of the affine transformation at time `t`.
24
+
25
+ The scheduler is responsible for providing the time-dependent parameters :math:`\alpha_t` and :math:`\sigma_t`, as well as their derivatives, which define the affine transformation at any given time `t`.
26
+
27
+ Using ``AffineProbPath`` in the flow matching framework:
28
+
29
+ .. code-block:: python
30
+
31
+ # Instantiates a probability path
32
+ my_path = AffineProbPath(...)
33
+ mse_loss = torch.nn.MSELoss()
34
+
35
+ for x_1 in dataset:
36
+ # Sets x_0 to random noise
37
+ x_0 = torch.randn()
38
+
39
+ # Sets t to a random value in [0,1]
40
+ t = torch.rand()
41
+
42
+ # Samples the conditional path X_t ~ p_t(X_t|X_0,X_1)
43
+ path_sample = my_path.sample(x_0=x_0, x_1=x_1, t=t)
44
+
45
+ # Computes the MSE loss w.r.t. the velocity
46
+ loss = mse_loss(path_sample.dx_t, my_model(x_t, t))
47
+ loss.backward()
48
+
49
+ Args:
50
+ scheduler (Scheduler): An instance of a scheduler that provides the parameters :math:`\alpha_t`, :math:`\sigma_t`, and their derivatives over time.
51
+
52
+ """
53
+
54
+ def __init__(self, scheduler: Scheduler):
55
+ self.scheduler = scheduler
56
+
57
+ def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> PathSample:
58
+ r"""Sample from the affine probability path:
59
+
60
+ | given :math:`(X_0,X_1) \sim \pi(X_0,X_1)` and a scheduler :math:`(\alpha_t,\sigma_t)`.
61
+ | return :math:`X_0, X_1, X_t = \alpha_t X_1 + \sigma_t X_0`, and the conditional velocity at :math:`X_t, \dot{X}_t = \dot{\alpha}_t X_1 + \dot{\sigma}_t X_0`.
62
+
63
+ Args:
64
+ x_0 (Tensor): source data point, shape (batch_size, ...).
65
+ x_1 (Tensor): target data point, shape (batch_size, ...).
66
+ t (Tensor): times in [0,1], shape (batch_size).
67
+
68
+ Returns:
69
+ PathSample: a conditional sample at :math:`X_t \sim p_t`.
70
+ """
71
+ self.assert_sample_shape(x_0=x_0, x_1=x_1, t=t)
72
+
73
+ scheduler_output = self.scheduler(t)
74
+
75
+ alpha_t = expand_tensor_like(
76
+ input_tensor=scheduler_output.alpha_t, expand_to=x_1
77
+ )
78
+ sigma_t = expand_tensor_like(
79
+ input_tensor=scheduler_output.sigma_t, expand_to=x_1
80
+ )
81
+ d_alpha_t = expand_tensor_like(
82
+ input_tensor=scheduler_output.d_alpha_t, expand_to=x_1
83
+ )
84
+ d_sigma_t = expand_tensor_like(
85
+ input_tensor=scheduler_output.d_sigma_t, expand_to=x_1
86
+ )
87
+
88
+ # construct xt ~ p_t(x|x1).
89
+ x_t = sigma_t * x_0 + alpha_t * x_1
90
+ dx_t = d_sigma_t * x_0 + d_alpha_t * x_1
91
+
92
+ return PathSample(x_t=x_t, dx_t=dx_t, x_1=x_1, x_0=x_0, t=t)
93
+
94
+ def target_to_velocity(self, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
95
+ r"""Convert from x_1 representation to velocity.
96
+
97
+ | given :math:`X_1`.
98
+ | return :math:`\dot{X}_t`.
99
+
100
+ Args:
101
+ x_1 (Tensor): target data point.
102
+ x_t (Tensor): path sample at time t.
103
+ t (Tensor): time in [0,1].
104
+
105
+ Returns:
106
+ Tensor: velocity.
107
+ """
108
+ scheduler_output = self.scheduler(t)
109
+
110
+ alpha_t = scheduler_output.alpha_t
111
+ d_alpha_t = scheduler_output.d_alpha_t
112
+ sigma_t = scheduler_output.sigma_t
113
+ d_sigma_t = scheduler_output.d_sigma_t
114
+
115
+ a_t = d_sigma_t / sigma_t
116
+ b_t = (d_alpha_t * sigma_t - d_sigma_t * alpha_t) / sigma_t
117
+
118
+ return a_t * x_t + b_t * x_1
119
+
120
+ def epsilon_to_velocity(self, epsilon: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
121
+ r"""Convert from epsilon representation to velocity.
122
+
123
+ | given :math:`\epsilon`.
124
+ | return :math:`\dot{X}_t`.
125
+
126
+ Args:
127
+ epsilon (Tensor): noise in the path sample.
128
+ x_t (Tensor): path sample at time t.
129
+ t (Tensor): time in [0,1].
130
+
131
+ Returns:
132
+ Tensor: velocity.
133
+ """
134
+ scheduler_output = self.scheduler(t)
135
+
136
+ alpha_t = scheduler_output.alpha_t
137
+ d_alpha_t = scheduler_output.d_alpha_t
138
+ sigma_t = scheduler_output.sigma_t
139
+ d_sigma_t = scheduler_output.d_sigma_t
140
+
141
+ a_t = d_alpha_t / alpha_t
142
+ b_t = (d_sigma_t * alpha_t - d_alpha_t * sigma_t) / alpha_t
143
+
144
+ return a_t * x_t + b_t * epsilon
145
+
146
+ def velocity_to_target(self, velocity: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
147
+ r"""Convert from velocity to x_1 representation.
148
+
149
+ | given :math:`\dot{X}_t`.
150
+ | return :math:`X_1`.
151
+
152
+ Args:
153
+ velocity (Tensor): velocity at the path sample.
154
+ x_t (Tensor): path sample at time t.
155
+ t (Tensor): time in [0,1].
156
+
157
+ Returns:
158
+ Tensor: target data point.
159
+ """
160
+ scheduler_output = self.scheduler(t)
161
+
162
+ alpha_t = scheduler_output.alpha_t
163
+ d_alpha_t = scheduler_output.d_alpha_t
164
+ sigma_t = scheduler_output.sigma_t
165
+ d_sigma_t = scheduler_output.d_sigma_t
166
+
167
+ a_t = -d_sigma_t / (d_alpha_t * sigma_t - d_sigma_t * alpha_t)
168
+ b_t = sigma_t / (d_alpha_t * sigma_t - d_sigma_t * alpha_t)
169
+
170
+ return a_t * x_t + b_t * velocity
171
+
172
+ def epsilon_to_target(self, epsilon: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
173
+ r"""Convert from epsilon representation to x_1 representation.
174
+
175
+ | given :math:`\epsilon`.
176
+ | return :math:`X_1`.
177
+
178
+ Args:
179
+ epsilon (Tensor): noise in the path sample.
180
+ x_t (Tensor): path sample at time t.
181
+ t (Tensor): time in [0,1].
182
+
183
+ Returns:
184
+ Tensor: target data point.
185
+ """
186
+ scheduler_output = self.scheduler(t)
187
+
188
+ alpha_t = scheduler_output.alpha_t
189
+ sigma_t = scheduler_output.sigma_t
190
+
191
+ a_t = 1 / alpha_t
192
+ b_t = -sigma_t / alpha_t
193
+
194
+ return a_t * x_t + b_t * epsilon
195
+
196
+ def velocity_to_epsilon(self, velocity: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
197
+ r"""Convert from velocity to noise representation.
198
+
199
+ | given :math:`\dot{X}_t`.
200
+ | return :math:`\epsilon`.
201
+
202
+ Args:
203
+ velocity (Tensor): velocity at the path sample.
204
+ x_t (Tensor): path sample at time t.
205
+ t (Tensor): time in [0,1].
206
+
207
+ Returns:
208
+ Tensor: noise in the path sample.
209
+ """
210
+ scheduler_output = self.scheduler(t)
211
+
212
+ alpha_t = scheduler_output.alpha_t
213
+ d_alpha_t = scheduler_output.d_alpha_t
214
+ sigma_t = scheduler_output.sigma_t
215
+ d_sigma_t = scheduler_output.d_sigma_t
216
+
217
+ a_t = -d_alpha_t / (d_sigma_t * alpha_t - d_alpha_t * sigma_t)
218
+ b_t = alpha_t / (d_sigma_t * alpha_t - d_alpha_t * sigma_t)
219
+
220
+ return a_t * x_t + b_t * velocity
221
+
222
+ def target_to_epsilon(self, x_1: Tensor, x_t: Tensor, t: Tensor) -> Tensor:
223
+ r"""Convert from x_1 representation to velocity.
224
+
225
+ | given :math:`X_1`.
226
+ | return :math:`\epsilon`.
227
+
228
+ Args:
229
+ x_1 (Tensor): target data point.
230
+ x_t (Tensor): path sample at time t.
231
+ t (Tensor): time in [0,1].
232
+
233
+ Returns:
234
+ Tensor: noise in the path sample.
235
+ """
236
+ scheduler_output = self.scheduler(t)
237
+
238
+ alpha_t = scheduler_output.alpha_t
239
+ sigma_t = scheduler_output.sigma_t
240
+
241
+ a_t = 1 / sigma_t
242
+ b_t = -alpha_t / sigma_t
243
+
244
+ return a_t * x_t + b_t * x_1
245
+
246
+
247
+ class CondOTProbPath(AffineProbPath):
248
+ r"""The ``CondOTProbPath`` class represents a conditional optimal transport probability path.
249
+
250
+ This class is a specialized version of the ``AffineProbPath`` that uses a conditional optimal transport scheduler to determine the parameters of the affine transformation.
251
+
252
+ The parameters :math:`\alpha_t` and :math:`\sigma_t` for the conditional optimal transport path are defined as:
253
+
254
+ .. math::
255
+
256
+ \alpha_t = t \quad \text{and} \quad \sigma_t = 1 - t.
257
+ """
258
+
259
+ def __init__(self):
260
+ self.scheduler = CondOTScheduler()
flow_matching/path/editflows_adapter.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from typing import List, Sequence, Optional, Tuple
3
+
4
+ import torch
5
+ from torch import Tensor
6
+ import pdb
7
+
8
+ from flow_matching.path import MixtureDiscreteProbPath
9
+
10
+
11
+ @dataclass
12
+ class EditFlowsPathSampleRagged:
13
+ # aligned inputs/outputs (ragged)
14
+ z0_list: List[Tensor] # list[(T_i,)]
15
+ z1_list: List[Tensor] # list[(T_i,)]
16
+ zt_list: List[Tensor] # list[(T_i,)]
17
+ # projected current sequences (ragged X_t)
18
+ x_t_list: List[Tensor] # list[(n_i,)]
19
+ # targets/masks for EF loss (ragged)
20
+ need_delete_list: List[Tensor] # list[(n_i,)] bool
21
+ need_substitute_list: List[Tensor] # list[(n_i,)] bool
22
+ sub_target_list: List[Tensor] # list[(n_i,)] long
23
+ ins_slot_idx_list: List[Tensor] # list[(K_i,)] long indices in [0..n_i]
24
+ ins_target_list: List[Tensor] # list[(K_i,)] long
25
+ # time
26
+ t: Tensor # (B,)
27
+
28
+
29
+ class EditFlowsPathAdapter:
30
+ """
31
+ Ragged adapter for Edit Flows:
32
+ - Accepts aligned Z sequences as tensors (B, N) (may include eps_id).
33
+ - Applies MixtureDiscreteProbPath's scheduler (σ_t) to sample z_t by
34
+ flipping z0→z1 with prob (1-σ_t) per aligned token.
35
+ - You can later project z_t -> x_t by removing ε.
36
+ """
37
+ def __init__(self, mixture_path, eps_id: int):
38
+ self.path = mixture_path
39
+ self.eps_id = int(eps_id)
40
+
41
+ # expose scheduler so training can precompute weights κ̇/(1-κ)
42
+ def scheduler(self, t: Tensor):
43
+ return self.path.scheduler(t)
44
+
45
+ @torch.no_grad()
46
+ def sample(
47
+ self,
48
+ z0: Tensor, # (B, N)
49
+ z1: Tensor, # (B, N)
50
+ t: Tensor, # () or (B,)
51
+ ) -> Tensor:
52
+ """
53
+ Sample z_t from (z0, z1) using the discrete mixture path:
54
+ - σ_t = scheduler(t)
55
+ - with prob σ_t keep z0
56
+ - with prob (1 - σ_t) take z1
57
+ We do this per-column.
58
+ BOS/EOS are already aligned -> z0 == z1 there -> safe.
59
+ """
60
+ device = z0.device
61
+ B, N = z0.shape
62
+
63
+ # get sigma_t from the path
64
+ sigma = self.path.scheduler(t).sigma_t # could be scalar or (B,)
65
+ if sigma.dim() == 0:
66
+ sigma = sigma.expand(B) # (B,)
67
+ else:
68
+ # ensure shape is (B,)
69
+ sigma = sigma.view(B)
70
+ # reshape to broadcast over columns
71
+ sigma = sigma.view(B, 1) # (B, 1)
72
+
73
+ # uniform noise per position
74
+ u = torch.rand(B, N, device=device)
75
+
76
+ # we only need to flip where z0 != z1
77
+ diff_mask = (z0 != z1) # (B, N)
78
+
79
+ # flip where u > sigma (i.e. prob 1 - sigma) AND tokens differ
80
+ flip = (u > sigma) & diff_mask # (B, N)
81
+
82
+ # choose from z1 when flip, else z0
83
+ z_t = torch.where(flip, z1, z0) # (B, N)
84
+
85
+ return z_t
flow_matching/path/geodesic.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import torch
8
+
9
+ from torch import Tensor
10
+ from torch.func import jvp, vmap
11
+
12
+ from flow_matching.path.path import ProbPath
13
+
14
+ from flow_matching.path.path_sample import PathSample
15
+ from flow_matching.path.scheduler import ConvexScheduler
16
+ from flow_matching.utils import expand_tensor_like
17
+
18
+ from flow_matching.utils.manifolds import geodesic, Manifold
19
+
20
+
21
+ class GeodesicProbPath(ProbPath):
22
+ r"""The ``GeodesicProbPath`` class represents a specific type of probability path where the transformation between distributions is defined through the geodesic path.
23
+ Mathematically, a geodesic path can be represented as:
24
+
25
+ .. math::
26
+
27
+ X_t = \psi_t(X_0 | X_1) = \exp_{X_1}(\kappa_t \log_{X_1}(X_0)),
28
+
29
+ where :math:`X_t` is the transformed data point at time `t`, :math:`X_0` and :math:`X_1` are the source and target data points, respectively, and :math:`\kappa_t` is a scheduler.
30
+
31
+ The scheduler is responsible for providing the time-dependent :math:`\kappa_t` and must be differentiable.
32
+
33
+ Using ``GeodesicProbPath`` in the flow matching framework:
34
+
35
+ .. code-block:: python
36
+ # Instantiates a manifold
37
+ manifold = FlatTorus()
38
+
39
+ # Instantiates a scheduler
40
+ scheduler = CondOTScheduler()
41
+
42
+ # Instantiates a probability path
43
+ my_path = GeodesicProbPath(scheduler, manifold)
44
+ mse_loss = torch.nn.MSELoss()
45
+
46
+ for x_1 in dataset:
47
+ # Sets x_0 to random noise
48
+ x_0 = torch.randn()
49
+
50
+ # Sets t to a random value in [0,1]
51
+ t = torch.rand()
52
+
53
+ # Samples the conditional path :math:`X_t \sim p_t(X_t|X_0,X_1)`
54
+ path_sample = my_path.sample(x_0=x_0, x_1=x_1, t=t)
55
+
56
+ # Computes the MSE loss w.r.t. the velocity
57
+ loss = mse_loss(path_sample.dx_t, my_model(x_t, t))
58
+ loss.backward()
59
+
60
+ Args:
61
+ scheduler (ConvexScheduler): The scheduler that provides :math:`\kappa_t`.
62
+ manifold (Manifold): The manifold on which the probability path is defined.
63
+
64
+ """
65
+
66
+ def __init__(self, scheduler: ConvexScheduler, manifold: Manifold):
67
+ self.scheduler = scheduler
68
+ self.manifold = manifold
69
+
70
+ def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> PathSample:
71
+ r"""Sample from the Riemannian probability path with geodesic interpolation:
72
+
73
+ | given :math:`(X_0,X_1) \sim \pi(X_0,X_1)` and a scheduler :math:`\kappa_t`.
74
+ | return :math:`X_0, X_1, X_t = \exp_{X_1}(\kappa_t \log_{X_1}(X_0))`, and the conditional velocity at :math:`X_t, \dot{X}_t`.
75
+
76
+ Args:
77
+ x_0 (Tensor): source data point, shape (batch_size, ...).
78
+ x_1 (Tensor): target data point, shape (batch_size, ...).
79
+ t (Tensor): times in [0,1], shape (batch_size).
80
+
81
+ Returns:
82
+ PathSample: A conditional sample at :math:`X_t \sim p_t`.
83
+ """
84
+ self.assert_sample_shape(x_0=x_0, x_1=x_1, t=t)
85
+ t = expand_tensor_like(input_tensor=t, expand_to=x_1[..., 0:1]).clone()
86
+
87
+ def cond_u(x_0, x_1, t):
88
+ path = geodesic(self.manifold, x_0, x_1)
89
+ x_t, dx_t = jvp(
90
+ lambda t: path(self.scheduler(t).alpha_t),
91
+ (t,),
92
+ (torch.ones_like(t).to(t),),
93
+ )
94
+ return x_t, dx_t
95
+
96
+ x_t, dx_t = vmap(cond_u)(x_0, x_1, t)
97
+ x_t = x_t.reshape_as(x_1)
98
+ dx_t = dx_t.reshape_as(x_1)
99
+
100
+ return PathSample(x_t=x_t, dx_t=dx_t, x_1=x_1, x_0=x_0, t=t)
flow_matching/path/mixture.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import torch
8
+ import torch.nn.functional as F
9
+
10
+ from torch import Tensor
11
+
12
+ from flow_matching.path.path import ProbPath
13
+
14
+ from flow_matching.path.path_sample import DiscretePathSample
15
+ from flow_matching.path.scheduler import ConvexScheduler
16
+ from flow_matching.utils import expand_tensor_like, unsqueeze_to_match
17
+
18
+
19
+ class MixtureDiscreteProbPath(ProbPath):
20
+ r"""The ``MixtureDiscreteProbPath`` class defines a factorized discrete probability path.
21
+
22
+ This path remains constant at the source data point :math:`X_0` until a random time, determined by the scheduler, when it flips to the target data point :math:`X_1`.
23
+ The scheduler determines the flip probability using the parameter :math:`\sigma_t`, which is a function of time `t`. Specifically, :math:`\sigma_t` represents the probability of remaining at :math:`X_0`, while :math:`1 - \sigma_t` is the probability of flipping to :math:`X_1`:
24
+
25
+ .. math::
26
+
27
+ P(X_t = X_0) = \sigma_t \quad \text{and} \quad P(X_t = X_1) = 1 - \sigma_t,
28
+
29
+ where :math:`\sigma_t` is provided by the scheduler.
30
+
31
+ Example:
32
+
33
+ .. code-block:: python
34
+
35
+ >>> x_0 = torch.zeros((1, 3, 3))
36
+ >>> x_1 = torch.ones((1, 3, 3))
37
+
38
+ >>> path = MixtureDiscreteProbPath(PolynomialConvexScheduler(n=1.0))
39
+ >>> result = path.sample(x_0, x_1, t=torch.tensor([0.1])).x_t
40
+ >>> result
41
+ tensor([[[0.0, 0.0, 0.0],
42
+ [0.0, 0.0, 1.0],
43
+ [0.0, 0.0, 0.0]]])
44
+
45
+ >>> result = path.sample(x_0, x_1, t=torch.tensor([0.5])).x_t
46
+ >>> result
47
+ tensor([[[1.0, 0.0, 1.0],
48
+ [0.0, 1.0, 0.0],
49
+ [0.0, 1.0, 0.0]]])
50
+
51
+ >>> result = path.sample(x_0, x_1, t=torch.tensor([1.0])).x_t
52
+ >>> result
53
+ tensor([[[1.0, 1.0, 1.0],
54
+ [1.0, 1.0, 1.0],
55
+ [1.0, 1.0, 1.0]]])
56
+
57
+ Args:
58
+ scheduler (ConvexScheduler): The scheduler that provides :math:`\sigma_t`.
59
+ """
60
+
61
+ def __init__(self, scheduler: ConvexScheduler):
62
+ assert isinstance(
63
+ scheduler, ConvexScheduler
64
+ ), "Scheduler for ConvexProbPath must be a ConvexScheduler."
65
+
66
+ self.scheduler = scheduler
67
+
68
+ def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> DiscretePathSample:
69
+ r"""Sample from the affine probability path:
70
+ | given :math:`(X_0,X_1) \sim \pi(X_0,X_1)` and a scheduler :math:`(\alpha_t,\sigma_t)`.
71
+ | return :math:`X_0, X_1, t`, and :math:`X_t \sim p_t`.
72
+ Args:
73
+ x_0 (Tensor): source data point, shape (batch_size, ...).
74
+ x_1 (Tensor): target data point, shape (batch_size, ...).
75
+ t (Tensor): times in [0,1], shape (batch_size).
76
+
77
+ Returns:
78
+ DiscretePathSample: a conditional sample at :math:`X_t ~ p_t`.
79
+ """
80
+ self.assert_sample_shape(x_0=x_0, x_1=x_1, t=t)
81
+
82
+ sigma_t = self.scheduler(t).sigma_t
83
+
84
+ sigma_t = expand_tensor_like(input_tensor=sigma_t, expand_to=x_1)
85
+
86
+ source_indices = torch.rand(size=x_1.shape, device=x_1.device) < sigma_t
87
+ x_t = torch.where(condition=source_indices, input=x_0, other=x_1)
88
+
89
+ return DiscretePathSample(x_t=x_t, x_1=x_1, x_0=x_0, t=t)
90
+
91
+ def posterior_to_velocity(
92
+ self, posterior_logits: Tensor, x_t: Tensor, t: Tensor
93
+ ) -> Tensor:
94
+ r"""Convert the factorized posterior to velocity.
95
+
96
+ | given :math:`p(X_1|X_t)`. In the factorized case: :math:`\prod_i p(X_1^i | X_t)`.
97
+ | return :math:`u_t`.
98
+
99
+ Args:
100
+ posterior_logits (Tensor): logits of the x_1 posterior conditional on x_t, shape (..., vocab size).
101
+ x_t (Tensor): path sample at time t, shape (...).
102
+ t (Tensor): time in [0,1].
103
+
104
+ Returns:
105
+ Tensor: velocity.
106
+ """
107
+ posterior = torch.softmax(posterior_logits, dim=-1)
108
+ vocabulary_size = posterior.shape[-1]
109
+ x_t = F.one_hot(x_t, num_classes=vocabulary_size)
110
+ t = unsqueeze_to_match(source=t, target=x_t)
111
+
112
+ scheduler_output = self.scheduler(t)
113
+
114
+ kappa_t = scheduler_output.alpha_t
115
+ d_kappa_t = scheduler_output.d_alpha_t
116
+
117
+ return (d_kappa_t / (1 - kappa_t)) * (posterior - x_t)
flow_matching/path/path.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from abc import ABC, abstractmethod
8
+
9
+ from torch import Tensor
10
+
11
+ from flow_matching.path.path_sample import PathSample
12
+
13
+
14
+ class ProbPath(ABC):
15
+ r"""Abstract class, representing a probability path.
16
+
17
+ A probability path transforms the distribution :math:`p(X_0)` into :math:`p(X_1)` over :math:`t=0\rightarrow 1`.
18
+
19
+ The ``ProbPath`` class is designed to support model training in the flow matching framework. It supports two key functionalities: (1) sampling the conditional probability path and (2) conversion between various training objectives.
20
+ Here is a high-level example
21
+
22
+ .. code-block:: python
23
+
24
+ # Instantiate a probability path
25
+ my_path = ProbPath(...)
26
+
27
+ for x_0, x_1 in dataset:
28
+ # Sets t to a random value in [0,1]
29
+ t = torch.rand()
30
+
31
+ # Samples the conditional path X_t ~ p_t(X_t|X_0,X_1)
32
+ path_sample = my_path.sample(x_0=x_0, x_1=x_1, t=t)
33
+
34
+ # Optimizes the model. The loss function varies, depending on model and path.
35
+ loss(path_sample, my_model(x_t, t)).backward()
36
+
37
+ """
38
+
39
+ @abstractmethod
40
+ def sample(self, x_0: Tensor, x_1: Tensor, t: Tensor) -> PathSample:
41
+ r"""Sample from an abstract probability path:
42
+
43
+ | given :math:`(X_0,X_1) \sim \pi(X_0,X_1)`.
44
+ | returns :math:`X_0, X_1, X_t \sim p_t(X_t)`, and a conditional target :math:`Y`, all objects are under ``PathSample``.
45
+
46
+ Args:
47
+ x_0 (Tensor): source data point, shape (batch_size, ...).
48
+ x_1 (Tensor): target data point, shape (batch_size, ...).
49
+ t (Tensor): times in [0,1], shape (batch_size).
50
+
51
+ Returns:
52
+ PathSample: a conditional sample.
53
+ """
54
+
55
+ def assert_sample_shape(self, x_0, x_1, t):
56
+ # x_0, x_1: list[Tensor] of variable length (ragged)
57
+ # t: Tensor of shape (B,)
58
+ assert isinstance(x_0, (list, tuple)), f"x_0 must be a list or tuple of ragged Tensors. Got {type(x_0)}"
59
+ assert isinstance(x_1, (list, tuple)), f"x_1 must be a list or tuple of ragged Tensors. Got {type(x_1)}"
60
+ assert isinstance(t, Tensor), f"t must be a torch.Tensor. Got {type(t)}"
61
+ batch_size = len(x_0)
62
+ assert len(x_1) == batch_size, f"x_1 must match x_0 batch size ({batch_size}). Got {len(x_1)}"
63
+ assert t.ndim == 1, f"The time vector t must have shape [batch_size]. Got {t.shape}."
64
+ assert t.shape[0] == batch_size, f"Time t dimension must match the batch size [{batch_size}]. Got {t.shape}"
flow_matching/path/path_sample.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from dataclasses import dataclass, field
8
+
9
+ from torch import Tensor
10
+
11
+
12
+ @dataclass
13
+ class PathSample:
14
+ r"""Represents a sample of a conditional-flow generated probability path.
15
+
16
+ Attributes:
17
+ x_1 (Tensor): the target sample :math:`X_1`.
18
+ x_0 (Tensor): the source sample :math:`X_0`.
19
+ t (Tensor): the time sample :math:`t`.
20
+ x_t (Tensor): samples :math:`X_t \sim p_t(X_t)`, shape (batch_size, ...).
21
+ dx_t (Tensor): conditional target :math:`\frac{\partial X}{\partial t}`, shape: (batch_size, ...).
22
+
23
+ """
24
+
25
+ x_1: Tensor = field(metadata={"help": "target samples X_1 (batch_size, ...)."})
26
+ x_0: Tensor = field(metadata={"help": "source samples X_0 (batch_size, ...)."})
27
+ t: Tensor = field(metadata={"help": "time samples t (batch_size, ...)."})
28
+ x_t: Tensor = field(
29
+ metadata={"help": "samples x_t ~ p_t(X_t), shape (batch_size, ...)."}
30
+ )
31
+ dx_t: Tensor = field(
32
+ metadata={"help": "conditional target dX_t, shape: (batch_size, ...)."}
33
+ )
34
+
35
+
36
+ @dataclass
37
+ class DiscretePathSample:
38
+ r"""
39
+ Represents a sample of a conditional-flow generated discrete probability path.
40
+
41
+ Attributes:
42
+ x_1 (Tensor): the target sample :math:`X_1`.
43
+ x_0 (Tensor): the source sample :math:`X_0`.
44
+ t (Tensor): the time sample :math:`t`.
45
+ x_t (Tensor): the sample along the path :math:`X_t \sim p_t`.
46
+ """
47
+
48
+ x_1: Tensor = field(metadata={"help": "target samples X_1 (batch_size, ...)."})
49
+ x_0: Tensor = field(metadata={"help": "source samples X_0 (batch_size, ...)."})
50
+ t: Tensor = field(metadata={"help": "time samples t (batch_size, ...)."})
51
+ x_t: Tensor = field(
52
+ metadata={"help": "samples X_t ~ p_t(X_t), shape (batch_size, ...)."}
53
+ )
flow_matching/path/scheduler/.DS_Store ADDED
Binary file (6.15 kB). View file
 
flow_matching/path/scheduler/__init__.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from .schedule_transform import ScheduleTransformedModel
8
+ from .scheduler import (
9
+ CondOTScheduler,
10
+ ConvexScheduler,
11
+ CosineScheduler,
12
+ LinearVPScheduler,
13
+ PolynomialConvexScheduler,
14
+ Scheduler,
15
+ SchedulerOutput,
16
+ VPScheduler,
17
+ )
18
+
19
+ __all__ = [
20
+ "CondOTScheduler",
21
+ "CosineScheduler",
22
+ "ConvexScheduler",
23
+ "PolynomialConvexScheduler",
24
+ "ScheduleTransformedModel",
25
+ "Scheduler",
26
+ "VPScheduler",
27
+ "LinearVPScheduler",
28
+ "SchedulerOutput",
29
+ ]
flow_matching/path/scheduler/schedule_transform.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from torch import Tensor
8
+
9
+ from flow_matching.path.scheduler.scheduler import Scheduler
10
+ from flow_matching.utils import ModelWrapper
11
+
12
+
13
+ class ScheduleTransformedModel(ModelWrapper):
14
+ """
15
+ Change of scheduler for a velocity model.
16
+
17
+ This class wraps a given velocity model and transforms its scheduling
18
+ to a new scheduler function. It modifies the time
19
+ dynamics of the model according to the new scheduler while maintaining
20
+ the original model's behavior.
21
+
22
+ Example:
23
+
24
+ .. code-block:: python
25
+
26
+ import torch
27
+ from flow_matching.path.scheduler import CondOTScheduler, CosineScheduler, ScheduleTransformedModel
28
+ from flow_matching.solver import ODESolver
29
+
30
+ # Initialize the model and schedulers
31
+ model = ...
32
+
33
+ original_scheduler = CondOTScheduler()
34
+ new_scheduler = CosineScheduler()
35
+
36
+ # Create the transformed model
37
+ transformed_model = ScheduleTransformedModel(
38
+ velocity_model=model,
39
+ original_scheduler=original_scheduler,
40
+ new_scheduler=new_scheduler
41
+ )
42
+
43
+ # Set up the solver
44
+ solver = ODESolver(velocity_model=transformed_model)
45
+
46
+ x_0 = torch.randn([10, 2]) # Example initial condition
47
+
48
+ x_1 = solver.sample(
49
+ time_steps=torch.tensor([0.0, 1.0]),
50
+ x_init=x_0,
51
+ step_size=1/1000
52
+ )[1]
53
+
54
+ Args:
55
+ velocity_model (ModelWrapper): The original velocity model to be transformed.
56
+ original_scheduler (Scheduler): The scheduler used by the original model. Must implement the snr_inverse function.
57
+ new_scheduler (Scheduler): The new scheduler to be applied to the model.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ velocity_model: ModelWrapper,
63
+ original_scheduler: Scheduler,
64
+ new_scheduler: Scheduler,
65
+ ):
66
+ super().__init__(model=velocity_model)
67
+ self.original_scheduler = original_scheduler
68
+ self.new_scheduler = new_scheduler
69
+
70
+ assert hasattr(self.original_scheduler, "snr_inverse") and callable(
71
+ getattr(self.original_scheduler, "snr_inverse")
72
+ ), "The original scheduler must have a callable 'snr_inverse' method."
73
+
74
+ def forward(self, x: Tensor, t: Tensor, **extras) -> Tensor:
75
+ r"""
76
+ Compute the transformed marginal velocity field for a new scheduler.
77
+ This method implements a post-training velocity scheduler change for
78
+ affine conditional flows. It transforms a generating marginal velocity
79
+ field :math:`u_t(x)` based on an original scheduler to a new marginal velocity
80
+ field :math:`\bar{u}_r(x)` based on a different scheduler, while maintaining
81
+ the same data coupling.
82
+ The transformation is based on the scale-time (ST) transformation
83
+ between the two conditional flows, defined as:
84
+
85
+ .. math::
86
+
87
+ \bar{X}_r = s_r X_{t_r},
88
+
89
+ where :math:`X_t` and :math:`\bar{X}_r` are defined by their respective schedulers.
90
+ The ST transformation is computed as:
91
+
92
+ .. math::
93
+
94
+ t_r = \rho^{-1}(\bar{\rho}(r)) \quad \text{and} \quad s_r = \frac{\bar{\sigma}_r}{\sigma_{t_r}}.
95
+
96
+ Here, :math:`\rho(t)` is the signal-to-noise ratio (SNR) defined as:
97
+
98
+ .. math::
99
+
100
+ \rho(t) = \frac{\alpha_t}{\sigma_t}.
101
+
102
+ :math:`\bar{\rho}(r)` is similarly defined for the new scheduler.
103
+ The marginal velocity for the new scheduler is then given by:
104
+
105
+ .. math::
106
+
107
+ \bar{u}_r(x) = \left(\frac{\dot{s}_r}{s_r}\right) x + s_r \dot{t}_r u_{t_r}\left(\frac{x}{s_r}\right).
108
+
109
+ Args:
110
+ x (Tensor): :math:`x_t`, the input tensor.
111
+ t (Tensor): The time tensor (denoted as :math:`r` above).
112
+ **extras: Additional arguments for the model.
113
+ Returns:
114
+ Tensor: The transformed velocity.
115
+ """
116
+ r = t
117
+
118
+ r_scheduler_output = self.new_scheduler(t=r)
119
+
120
+ alpha_r = r_scheduler_output.alpha_t
121
+ sigma_r = r_scheduler_output.sigma_t
122
+ d_alpha_r = r_scheduler_output.d_alpha_t
123
+ d_sigma_r = r_scheduler_output.d_sigma_t
124
+
125
+ t = self.original_scheduler.snr_inverse(alpha_r / sigma_r)
126
+
127
+ t_scheduler_output = self.original_scheduler(t=t)
128
+
129
+ alpha_t = t_scheduler_output.alpha_t
130
+ sigma_t = t_scheduler_output.sigma_t
131
+ d_alpha_t = t_scheduler_output.d_alpha_t
132
+ d_sigma_t = t_scheduler_output.d_sigma_t
133
+
134
+ s_r = sigma_r / sigma_t
135
+
136
+ dt_r = (
137
+ sigma_t
138
+ * sigma_t
139
+ * (sigma_r * d_alpha_r - alpha_r * d_sigma_r)
140
+ / (sigma_r * sigma_r * (sigma_t * d_alpha_t - alpha_t * d_sigma_t))
141
+ )
142
+
143
+ ds_r = (sigma_t * d_sigma_r - sigma_r * d_sigma_t * dt_r) / (sigma_t * sigma_t)
144
+
145
+ u_t = self.model(x=x / s_r, t=t, **extras)
146
+ u_r = ds_r * x / s_r + dt_r * s_r * u_t
147
+
148
+ return u_r
flow_matching/path/scheduler/scheduler.py ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from abc import ABC, abstractmethod
8
+ from dataclasses import dataclass, field
9
+
10
+ from typing import Union
11
+
12
+ import torch
13
+
14
+ from torch import Tensor
15
+
16
+
17
+ @dataclass
18
+ class SchedulerOutput:
19
+ r"""Represents a sample of a conditional-flow generated probability path.
20
+
21
+ Attributes:
22
+ alpha_t (Tensor): :math:`\alpha_t`, shape (...).
23
+ sigma_t (Tensor): :math:`\sigma_t`, shape (...).
24
+ d_alpha_t (Tensor): :math:`\frac{\partial}{\partial t}\alpha_t`, shape (...).
25
+ d_sigma_t (Tensor): :math:`\frac{\partial}{\partial t}\sigma_t`, shape (...).
26
+
27
+ """
28
+
29
+ alpha_t: Tensor = field(metadata={"help": "alpha_t"})
30
+ sigma_t: Tensor = field(metadata={"help": "sigma_t"})
31
+ d_alpha_t: Tensor = field(metadata={"help": "Derivative of alpha_t."})
32
+ d_sigma_t: Tensor = field(metadata={"help": "Derivative of sigma_t."})
33
+
34
+
35
+ class Scheduler(ABC):
36
+ """Base Scheduler class."""
37
+
38
+ @abstractmethod
39
+ def __call__(self, t: Tensor) -> SchedulerOutput:
40
+ r"""
41
+ Args:
42
+ t (Tensor): times in [0,1], shape (...).
43
+
44
+ Returns:
45
+ SchedulerOutput: :math:`\alpha_t,\sigma_t,\frac{\partial}{\partial t}\alpha_t,\frac{\partial}{\partial t}\sigma_t`
46
+ """
47
+ ...
48
+
49
+ @abstractmethod
50
+ def snr_inverse(self, snr: Tensor) -> Tensor:
51
+ r"""
52
+ Computes :math:`t` from the signal-to-noise ratio :math:`\frac{\alpha_t}{\sigma_t}`.
53
+
54
+ Args:
55
+ snr (Tensor): The signal-to-noise, shape (...)
56
+
57
+ Returns:
58
+ Tensor: t, shape (...)
59
+ """
60
+ ...
61
+
62
+
63
+ class ConvexScheduler(Scheduler):
64
+ @abstractmethod
65
+ def __call__(self, t: Tensor) -> SchedulerOutput:
66
+ r"""Scheduler for convex paths.
67
+
68
+ Args:
69
+ t (Tensor): times in [0,1], shape (...).
70
+
71
+ Returns:
72
+ SchedulerOutput: :math:`\alpha_t,\sigma_t,\frac{\partial}{\partial t}\alpha_t,\frac{\partial}{\partial t}\sigma_t`
73
+ """
74
+ ...
75
+
76
+ @abstractmethod
77
+ def kappa_inverse(self, kappa: Tensor) -> Tensor:
78
+ r"""
79
+ Computes :math:`t` from :math:`\kappa_t`.
80
+
81
+ Args:
82
+ kappa (Tensor): :math:`\kappa`, shape (...)
83
+
84
+ Returns:
85
+ Tensor: t, shape (...)
86
+ """
87
+ ...
88
+
89
+ def snr_inverse(self, snr: Tensor) -> Tensor:
90
+ r"""
91
+ Computes :math:`t` from the signal-to-noise ratio :math:`\frac{\alpha_t}{\sigma_t}`.
92
+
93
+ Args:
94
+ snr (Tensor): The signal-to-noise, shape (...)
95
+
96
+ Returns:
97
+ Tensor: t, shape (...)
98
+ """
99
+ kappa_t = snr / (1.0 + snr)
100
+
101
+ return self.kappa_inverse(kappa=kappa_t)
102
+
103
+
104
+ class CondOTScheduler(ConvexScheduler):
105
+ """CondOT Scheduler."""
106
+
107
+ def __call__(self, t: Tensor) -> SchedulerOutput:
108
+ return SchedulerOutput(
109
+ alpha_t=t,
110
+ sigma_t=1 - t,
111
+ d_alpha_t=torch.ones_like(t),
112
+ d_sigma_t=-torch.ones_like(t),
113
+ )
114
+
115
+ def kappa_inverse(self, kappa: Tensor) -> Tensor:
116
+ return kappa
117
+
118
+
119
+ class PolynomialConvexScheduler(ConvexScheduler):
120
+ """Polynomial Scheduler."""
121
+
122
+ def __init__(self, n: Union[float, int]) -> None:
123
+ assert isinstance(
124
+ n, (float, int)
125
+ ), f"`n` must be a float or int. Got {type(n)=}."
126
+ assert n > 0, f"`n` must be positive. Got {n=}."
127
+
128
+ self.n = n
129
+
130
+ def __call__(self, t: Tensor) -> SchedulerOutput:
131
+ return SchedulerOutput(
132
+ alpha_t=t**self.n,
133
+ sigma_t=1 - t**self.n,
134
+ d_alpha_t=self.n * (t ** (self.n - 1)),
135
+ d_sigma_t=-self.n * (t ** (self.n - 1)),
136
+ )
137
+
138
+ def kappa_inverse(self, kappa: Tensor) -> Tensor:
139
+ return torch.pow(kappa, 1.0 / self.n)
140
+
141
+
142
+ class VPScheduler(Scheduler):
143
+ """Variance Preserving Scheduler."""
144
+
145
+ def __init__(self, beta_min: float = 0.1, beta_max: float = 20.0) -> None:
146
+ self.beta_min = beta_min
147
+ self.beta_max = beta_max
148
+ super().__init__()
149
+
150
+ def __call__(self, t: Tensor) -> SchedulerOutput:
151
+ b = self.beta_min
152
+ B = self.beta_max
153
+ T = 0.5 * (1 - t) ** 2 * (B - b) + (1 - t) * b
154
+ dT = -(1 - t) * (B - b) - b
155
+
156
+ return SchedulerOutput(
157
+ alpha_t=torch.exp(-0.5 * T),
158
+ sigma_t=torch.sqrt(1 - torch.exp(-T)),
159
+ d_alpha_t=-0.5 * dT * torch.exp(-0.5 * T),
160
+ d_sigma_t=0.5 * dT * torch.exp(-T) / torch.sqrt(1 - torch.exp(-T)),
161
+ )
162
+
163
+ def snr_inverse(self, snr: Tensor) -> Tensor:
164
+ T = -torch.log(snr**2 / (snr**2 + 1))
165
+ b = self.beta_min
166
+ B = self.beta_max
167
+ t = 1 - ((-b + torch.sqrt(b**2 + 2 * (B - b) * T)) / (B - b))
168
+ return t
169
+
170
+
171
+ class LinearVPScheduler(Scheduler):
172
+ """Linear Variance Preserving Scheduler."""
173
+
174
+ def __call__(self, t: Tensor) -> SchedulerOutput:
175
+ return SchedulerOutput(
176
+ alpha_t=t,
177
+ sigma_t=(1 - t**2) ** 0.5,
178
+ d_alpha_t=torch.ones_like(t),
179
+ d_sigma_t=-t / (1 - t**2) ** 0.5,
180
+ )
181
+
182
+ def snr_inverse(self, snr: Tensor) -> Tensor:
183
+ return torch.sqrt(snr**2 / (1 + snr**2))
184
+
185
+
186
+ class CosineScheduler(Scheduler):
187
+ """Cosine Scheduler."""
188
+
189
+ def __call__(self, t: Tensor) -> SchedulerOutput:
190
+ pi = torch.pi
191
+ return SchedulerOutput(
192
+ alpha_t=torch.sin(pi / 2 * t),
193
+ sigma_t=torch.cos(pi / 2 * t),
194
+ d_alpha_t=pi / 2 * torch.cos(pi / 2 * t),
195
+ d_sigma_t=-pi / 2 * torch.sin(pi / 2 * t),
196
+ )
197
+
198
+ def snr_inverse(self, snr: Tensor) -> Tensor:
199
+ return 2.0 * torch.atan(snr) / torch.pi
flow_matching/solver/.DS_Store ADDED
Binary file (6.15 kB). View file
 
flow_matching/solver/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from .discrete_solver import MixtureDiscreteEulerSolver
8
+ from .ode_solver import ODESolver
9
+ from .riemannian_ode_solver import RiemannianODESolver
10
+ from .solver import Solver
11
+ from .editflows_solver import EditFlowsEulerSolver
12
+
13
+ __all__ = [
14
+ "ODESolver",
15
+ "Solver",
16
+ "ModelWrapper",
17
+ "MixtureDiscreteEulerSolver",
18
+ "RiemannianODESolver",
19
+ "EditFlowsEulerSolver",
20
+ ]
flow_matching/solver/discrete_solver.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from contextlib import nullcontext
8
+ from math import ceil
9
+ from typing import Callable, Optional, Union
10
+
11
+ import torch
12
+ from torch import Tensor
13
+
14
+ from torch.nn import functional as F
15
+
16
+ from flow_matching.path import MixtureDiscreteProbPath
17
+
18
+ from flow_matching.solver.solver import Solver
19
+ from flow_matching.utils import categorical, ModelWrapper
20
+ from .utils import get_nearest_times
21
+
22
+ try:
23
+ from tqdm import tqdm
24
+
25
+ TQDM_AVAILABLE = True
26
+ except ImportError:
27
+ TQDM_AVAILABLE = False
28
+
29
+
30
+ class MixtureDiscreteEulerSolver(Solver):
31
+ r"""Solver that simulates the CTMC process :math:`(X_t)_{t_{\text{init}}\leq t\leq t_{\text{final}}}` defined by :math:`p_t` the marginal probability path of ``path``.
32
+ Given :math:`X_t \sim p_t`, the algorithm of solver step from :math:`t` to :math:`t+h` for the i-th coordinate is:
33
+
34
+ .. math::
35
+
36
+ \begin{align*}
37
+ & X_1^i \sim p_{1|t}^i(\cdot|X_t)\\
38
+ & \lambda^i \gets \sum_{x^i\ne X_t^i} u_t^i(x^i, X_t^i|X_1^i)\\
39
+ & Z^i_{\text{change}} \sim U[0,1]\\
40
+ & X_{t+h}^i \sim \begin{cases}
41
+ \frac{u_t^i(\cdot, X_t^i|X_1^i)}{\lambda^i}(1-\delta_{X_t^i}(\cdot)) \text{ if $Z^i_{\text{change}}\le 1-e^{-h\lambda^i}$}\\
42
+ \delta_{X_t^i}(\cdot) \text{ else }
43
+ \end{cases}
44
+ \end{align*}
45
+
46
+ Where :math:`p_{1|t}(\cdot|X_t)` is the output of ``model``, and the conditional probability velocity is of the mixture probability path is:
47
+
48
+ .. math::
49
+
50
+ u_t^i(x^i, y^i|x_1^i) = \hat{u}_t^i(x^i, y^i|x_1^i) + c_{\text{div\_free}}\left[\hat{u}_t^i(x^i, y^i|x_1^i) - \check{u}_t^i(x^i, y^i|x_1^i) \right],
51
+
52
+ where
53
+
54
+ .. math::
55
+ \hat{u}_t^i(x^i, y^i|x_1^i) = \frac{\dot{\kappa}_t}{1-\kappa_t} \left[ \delta_{x_1^i}(x^i) - \delta_{y^i}(x^i) \right],
56
+
57
+ and
58
+
59
+ .. math::
60
+
61
+ \check{u}_t^i(x^i, y^i|x_1^i) = \frac{\dot{\kappa}_t}{\kappa_t}\left[ \delta_{y^i}(x^i) - p(x^i) \right].
62
+
63
+ The source distribution :math:`p(x^i)` is given by ``p``.
64
+
65
+ Args:
66
+ model (ModelWrapper): trained with x-prediction, outputting posterior probabilities (in the range :math:`[0,1]`), output must be [..., vocabulary_size].
67
+ path (MixtureDiscreteProbPath): Probability path used for x-prediction training.
68
+ vocabulary_size (int): size of the discrete vocabulary.
69
+ source_distribution_p (Optional[Tensor], optional): Source distribution, must be of shape [vocabulary_size]. Required only when divergence-free term for the probability velocity is non-zero. Defaults to None.
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ model: ModelWrapper,
75
+ path: MixtureDiscreteProbPath,
76
+ vocabulary_size: int,
77
+ source_distribution_p: Optional[Tensor] = None,
78
+ ):
79
+ super().__init__()
80
+ self.model = model
81
+ self.path = path
82
+ self.vocabulary_size = vocabulary_size
83
+
84
+ if source_distribution_p is not None:
85
+ assert source_distribution_p.shape == torch.Size(
86
+ [vocabulary_size]
87
+ ), f"Source distribution p dimension must match the vocabulary size {vocabulary_size}. Got {source_distribution_p.shape}."
88
+
89
+ self.source_distribution_p = source_distribution_p
90
+
91
+ @torch.no_grad()
92
+ def sample(
93
+ self,
94
+ x_init: Tensor,
95
+ step_size: Optional[float],
96
+ div_free: Union[float, Callable[[float], float]] = 0.0,
97
+ dtype_categorical: torch.dtype = torch.float32,
98
+ time_grid: Tensor = torch.tensor([0.0, 1.0]),
99
+ return_intermediates: bool = False,
100
+ verbose: bool = False,
101
+ **model_extras,
102
+ ) -> Tensor:
103
+ """
104
+ Sample a sequence of discrete values from the given model.
105
+
106
+ .. code-block:: python
107
+
108
+ import torch
109
+ from flow_matching.utils import ModelWrapper
110
+ from flow_matching.solver import MixtureDiscreteEulerSolver
111
+
112
+ class DummyModel(ModelWrapper):
113
+ def __init__(self):
114
+ super().__init__(None)
115
+ def forward(self, x: torch.Tensor, t: torch.Tensor, **extras) -> torch.Tensor:
116
+ return ...
117
+
118
+ model = DummyModel()
119
+ solver = MixtureDiscreteEulerSolver(model=model)
120
+
121
+ x_init = torch.LongTensor([122, 725])
122
+ step_size = 0.001
123
+ time_grid = torch.tensor([0.0, 1.0])
124
+
125
+ result = solver.sample(x_init=x_init, step_size=step_size, time_grid=time_grid)
126
+
127
+ Args:
128
+ x_init (Tensor): The initial state.
129
+ step_size (Optional[float]): If float then time discretization is uniform with the given step size. If None then time discretization is set to be time_grid.
130
+ div_free (Union[float, Callable[[float], float]]): The coefficient of the divergence-free term in the probability velocity. Can be either a float or a time dependent function. Defaults to 0.0.
131
+ dtype_categorical (torch.dtype): Precision to use for categorical sampler. Defaults to torch.float32.
132
+ time_grid (Tensor): The CTMC process is solved in the interval [time_grid[0], time_grid[-1]] and if step_size is None then time discretization is set by the time grid. Defaults to torch.tensor([0.0,1.0]).
133
+ return_intermediates (bool): If True then return intermediate time steps according to time_grid. Defaults to False.
134
+ verbose (bool): Whether to print progress bars. Defaults to False.
135
+ **model_extras: Additional input for the model.
136
+
137
+ Returns:
138
+ Tensor: The sampled sequence of discrete values.
139
+
140
+ Raises:
141
+ ImportError: To run in verbose mode, tqdm must be installed.
142
+ """
143
+ if not div_free == 0.0:
144
+ assert (
145
+ self.source_distribution_p is not None
146
+ ), "Source distribution p must be specified in order to add a divergence-free term to the probability velocity."
147
+
148
+ # Initialize the current state `x_t` with the initial state `X_0`.
149
+ time_grid = time_grid.to(device=x_init.device)
150
+
151
+ if step_size is None:
152
+ # If step_size is None then set the t discretization to time_grid.
153
+ t_discretization = time_grid
154
+ n_steps = len(time_grid) - 1
155
+ else:
156
+ # If step_size is float then t discretization is uniform with step size set by step_size.
157
+ t_init = time_grid[0].item()
158
+ t_final = time_grid[-1].item()
159
+ assert (
160
+ t_final - t_init
161
+ ) > step_size, f"Time interval [time_grid[0], time_grid[-1]] must be larger than step_size. Got a time interval [{t_init}, {t_final}] and step_size {step_size}."
162
+
163
+ n_steps = ceil((t_final - t_init) / step_size)
164
+ t_discretization = torch.tensor(
165
+ [t_init + step_size * i for i in range(n_steps)] + [t_final],
166
+ device=x_init.device,
167
+ )
168
+
169
+ if return_intermediates:
170
+ # get order of intermediate steps:
171
+ order = torch.argsort(time_grid)
172
+ # Compute intermediate steps to return via nearest points in t_discretization to time_grid.
173
+ time_grid = get_nearest_times(
174
+ time_grid=time_grid, t_discretization=t_discretization
175
+ )
176
+
177
+ x_t = x_init.clone()
178
+ steps_counter = 0
179
+ res = []
180
+
181
+ if return_intermediates:
182
+ res = [x_init.clone()]
183
+
184
+ if verbose:
185
+ if not TQDM_AVAILABLE:
186
+ raise ImportError(
187
+ "tqdm is required for verbose mode. Please install it."
188
+ )
189
+ ctx = tqdm(total=t_final, desc=f"NFE: {steps_counter}")
190
+ else:
191
+ ctx = nullcontext()
192
+
193
+ with ctx:
194
+ for i in range(n_steps):
195
+ t = t_discretization[i : i + 1]
196
+ h = t_discretization[i + 1 : i + 2] - t_discretization[i : i + 1]
197
+
198
+ # Sample x_1 ~ p_1|t( \cdot |x_t)
199
+ p_1t = self.model(x=x_t, t=t.repeat(x_t.shape[0]), **model_extras)
200
+ x_1 = categorical(p_1t.to(dtype=dtype_categorical))
201
+
202
+ # Checks if final step
203
+ if i == n_steps - 1:
204
+ x_t = x_1
205
+ else:
206
+ # Compute u_t(x|x_t,x_1)
207
+ scheduler_output = self.path.scheduler(t=t)
208
+
209
+ k_t = scheduler_output.alpha_t
210
+ d_k_t = scheduler_output.d_alpha_t
211
+
212
+ delta_1 = F.one_hot(x_1, num_classes=self.vocabulary_size).to(
213
+ k_t.dtype
214
+ )
215
+ u = d_k_t / (1 - k_t) * delta_1
216
+
217
+ # Add divergence-free part
218
+ div_free_t = div_free(t) if callable(div_free) else div_free
219
+
220
+ if div_free_t > 0:
221
+ p_0 = self.source_distribution_p[(None,) * x_t.dim()]
222
+ u = u + div_free_t * d_k_t / (k_t * (1 - k_t)) * (
223
+ (1 - k_t) * p_0 + k_t * delta_1
224
+ )
225
+
226
+ # Set u_t(x_t|x_t,x_1) = 0
227
+ delta_t = F.one_hot(x_t, num_classes=self.vocabulary_size)
228
+ u = torch.where(
229
+ delta_t.to(dtype=torch.bool), torch.zeros_like(u), u
230
+ )
231
+
232
+ # Sample x_t ~ u_t( \cdot |x_t,x_1)
233
+ intensity = u.sum(dim=-1) # Assuming u_t(xt|xt,x1) := 0
234
+ mask_jump = torch.rand(
235
+ size=x_t.shape, device=x_t.device
236
+ ) < 1 - torch.exp(-h * intensity)
237
+
238
+ if mask_jump.sum() > 0:
239
+ x_t[mask_jump] = categorical(
240
+ u[mask_jump].to(dtype=dtype_categorical)
241
+ )
242
+
243
+ steps_counter += 1
244
+ t = t + h
245
+
246
+ if return_intermediates and (t in time_grid):
247
+ res.append(x_t.clone())
248
+
249
+ if verbose:
250
+ ctx.n = t.item()
251
+ ctx.refresh()
252
+ ctx.set_description(f"NFE: {steps_counter}")
253
+
254
+ if return_intermediates:
255
+ if step_size is None:
256
+ return torch.stack(res, dim=0)
257
+ else:
258
+ return torch.stack(res, dim=0)[order]
259
+ else:
260
+ return x_t
flow_matching/solver/editflows_solver.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc.
2
+ # All rights reserved.
3
+
4
+ from typing import Callable, List, Sequence, Tuple, Optional
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from torch import Tensor
9
+
10
+
11
+ class EditFlowsEulerSolver:
12
+ """
13
+ Ragged EditFlows CTMC solver with Euler thinning (≤1 jump per step).
14
+
15
+ At each step h:
16
+ - query model for (λ_ins[slots], Q_ins[slots], λ_del[tokens], λ_sub[tokens], Q_sub[tokens])
17
+ - total intensity Λ = sum(λ_ins) + sum(λ_del) + sum(λ_sub)
18
+ - with prob 1 - exp(-h Λ), take ONE jump:
19
+ * sample which event from concatenated rates
20
+ * if ins/sub: sample token from the Q at that position/slot
21
+ * apply edit to the ragged sequence (insert/delete/replace)
22
+ - advance t ← t + h
23
+
24
+ This is faithful to the CTMC in the small-h regime used by Euler schemes.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ model, # callable: (x_list, t_vec) -> ragged heads
30
+ scheduler: Callable[[Tensor], object], # path.scheduler(t) providing alpha_t, d_alpha_t, sigma_t if needed
31
+ vocab_size: int,
32
+ dtype_categorical: torch.dtype = torch.float64,
33
+ generator: Optional[torch.Generator] = None,
34
+ ) -> None:
35
+ self.model = model
36
+ self.scheduler = scheduler
37
+ self.vocab_size = vocab_size
38
+ self.dtype_cat = dtype_categorical
39
+ self.gen = generator
40
+
41
+ @torch.no_grad()
42
+ def _sample_categorical_logits(self, logits: Tensor) -> Tensor:
43
+ # logits: (V,) -> sample id
44
+ probs = F.softmax(logits, dim=-1, dtype=self.dtype_cat)
45
+ return torch.multinomial(probs, num_samples=1, replacement=True, generator=self.gen).squeeze(0)
46
+
47
+ @torch.no_grad()
48
+ def _maybe_one_jump(
49
+ self,
50
+ x: Tensor, # (n,)
51
+ lam_ins: Tensor, # (n+1,)
52
+ logits_ins: Tensor, # (n+1, V)
53
+ lam_del: Tensor, # (n,)
54
+ lam_sub: Tensor, # (n,)
55
+ logits_sub: Tensor, # (n, V)
56
+ h: float,
57
+ device: torch.device,
58
+ ) -> Tensor:
59
+ """
60
+ Apply at most one edit event to x with step size h.
61
+ Returns possibly modified x (1-D LongTensor).
62
+ """
63
+ n = x.numel()
64
+ # Total intensity
65
+ sum_ins = lam_ins.sum() if lam_ins.numel() else torch.tensor(0.0, device=device)
66
+ sum_del = lam_del.sum() if lam_del.numel() else torch.tensor(0.0, device=device)
67
+ sum_sub = lam_sub.sum() if lam_sub.numel() else torch.tensor(0.0, device=device)
68
+ total = (sum_ins + sum_del + sum_sub).clamp(min=0.0)
69
+
70
+ if total.item() <= 0:
71
+ return x # no possible jumps
72
+
73
+ # Bernoulli: does a jump occur?
74
+ p_jump = 1.0 - torch.exp(torch.tensor(-h, device=device) * total)
75
+ if torch.rand((), device=device, generator=self.gen) >= p_jump:
76
+ return x # no jump this step
77
+
78
+ # Build event vector: [ins(0..n), del(0..n-1), sub(0..n-1)]
79
+ parts = []
80
+ if lam_ins.numel(): parts.append(lam_ins)
81
+ if lam_del.numel(): parts.append(lam_del)
82
+ if lam_sub.numel(): parts.append(lam_sub)
83
+ lam_cat = torch.cat(parts, dim=0) # (n+1 + n + n,)
84
+ probs = (lam_cat / total).to(self.dtype_cat)
85
+
86
+ # Sample which event
87
+ e_idx = torch.multinomial(probs, num_samples=1, generator=self.gen).item()
88
+
89
+ # Decode which branch
90
+ offset = 0
91
+ if e_idx < lam_ins.numel():
92
+ # INSERT at slot s
93
+ s = e_idx
94
+ # sample token from slot distribution
95
+ y = self._sample_categorical_logits(logits_ins[s]) # id ∈ [0..V-1]
96
+ # perform insertion at slot s: x' = [x[:s], y, x[s:]]
97
+ if s == 0:
98
+ x_new = torch.cat([y.view(1), x], dim=0)
99
+ elif s == n:
100
+ x_new = torch.cat([x, y.view(1)], dim=0)
101
+ else:
102
+ x_new = torch.cat([x[:s], y.view(1), x[s:]], dim=0)
103
+ return x_new
104
+
105
+ e_idx -= lam_ins.numel()
106
+ if e_idx < lam_del.numel():
107
+ # DELETE token j
108
+ j = e_idx
109
+ if n == 0:
110
+ return x
111
+ if j == 0:
112
+ return x[1:]
113
+ elif j == n - 1:
114
+ return x[:-1]
115
+ else:
116
+ return torch.cat([x[:j], x[j + 1 :]], dim=0)
117
+
118
+ e_idx -= lam_del.numel()
119
+ # SUBSTITUTE at token j
120
+ j = e_idx
121
+ if n == 0:
122
+ return x
123
+ y = self._sample_categorical_logits(logits_sub[j])
124
+ x_new = x.clone()
125
+ x_new[j] = y
126
+ return x_new
127
+
128
+ @torch.no_grad()
129
+ def sample(
130
+ self,
131
+ x_list: Sequence[Tensor], # ragged: list[(n_i,)]
132
+ n_steps: int,
133
+ t0: float = 0.0,
134
+ t1: float = 1.0,
135
+ verbose: bool = False,
136
+ ) -> List[Tensor]:
137
+ """
138
+ Evolve all sequences independently with shared time grid t_k.
139
+ """
140
+ assert n_steps >= 1
141
+ B = len(x_list)
142
+ device = x_list[0].device if B > 0 else torch.device("cpu")
143
+
144
+ x = [xi.clone() for xi in x_list]
145
+ t = torch.full((B,), float(t0), device=device)
146
+ h = (t1 - t0) / float(n_steps)
147
+
148
+ for k in range(n_steps):
149
+ # Query model once at time t_k (vector of size B)
150
+ lam_ins, logits_ins, lam_del, lam_sub, logits_sub = self.model(x, t)
151
+
152
+ # Apply at most one jump per sample
153
+ for i in range(B):
154
+ x[i] = self._maybe_one_jump(
155
+ x=x[i],
156
+ lam_ins=lam_ins[i].squeeze(0) if lam_ins[i].dim() == 2 else lam_ins[i],
157
+ logits_ins=logits_ins[i].squeeze(0) if logits_ins[i].dim() == 3 else logits_ins[i],
158
+ lam_del=lam_del[i].squeeze(0) if lam_del[i].dim() == 2 else lam_del[i],
159
+ lam_sub=lam_sub[i].squeeze(0) if lam_sub[i].dim() == 2 else lam_sub[i],
160
+ logits_sub=logits_sub[i].squeeze(0) if logits_sub[i].dim() == 3 else logits_sub[i],
161
+ h=h,
162
+ device=device,
163
+ )
164
+
165
+ # advance time
166
+ t = t + h
167
+
168
+ return x
flow_matching/solver/ode_solver.py ADDED
@@ -0,0 +1,197 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from typing import Callable, Optional, Sequence, Tuple, Union
8
+
9
+ import torch
10
+ from torch import Tensor
11
+ from torchdiffeq import odeint
12
+
13
+ from flow_matching.solver.solver import Solver
14
+ from flow_matching.utils import gradient, ModelWrapper
15
+
16
+
17
+ class ODESolver(Solver):
18
+ """A class to solve ordinary differential equations (ODEs) using a specified velocity model.
19
+
20
+ This class utilizes a velocity field model to solve ODEs over a given time grid using numerical ode solvers.
21
+
22
+ Args:
23
+ velocity_model (Union[ModelWrapper, Callable]): a velocity field model receiving :math:`(x,t)` and returning :math:`u_t(x)`
24
+ """
25
+
26
+ def __init__(self, velocity_model: Union[ModelWrapper, Callable]):
27
+ super().__init__()
28
+ self.velocity_model = velocity_model
29
+
30
+ def sample(
31
+ self,
32
+ x_init: Tensor,
33
+ step_size: Optional[float],
34
+ method: str = "euler",
35
+ atol: float = 1e-5,
36
+ rtol: float = 1e-5,
37
+ time_grid: Tensor = torch.tensor([0.0, 1.0]),
38
+ return_intermediates: bool = False,
39
+ enable_grad: bool = False,
40
+ **model_extras,
41
+ ) -> Union[Tensor, Sequence[Tensor]]:
42
+ r"""Solve the ODE with the velocity field.
43
+
44
+ Example:
45
+
46
+ .. code-block:: python
47
+
48
+ import torch
49
+ from flow_matching.utils import ModelWrapper
50
+ from flow_matching.solver import ODESolver
51
+
52
+ class DummyModel(ModelWrapper):
53
+ def __init__(self):
54
+ super().__init__(None)
55
+
56
+ def forward(self, x: torch.Tensor, t: torch.Tensor, **extras) -> torch.Tensor:
57
+ return torch.ones_like(x) * 3.0 * t**2
58
+
59
+ velocity_model = DummyModel()
60
+ solver = ODESolver(velocity_model=velocity_model)
61
+ x_init = torch.tensor([0.0, 0.0])
62
+ step_size = 0.001
63
+ time_grid = torch.tensor([0.0, 1.0])
64
+
65
+ result = solver.sample(x_init=x_init, step_size=step_size, time_grid=time_grid)
66
+
67
+ Args:
68
+ x_init (Tensor): initial conditions (e.g., source samples :math:`X_0 \sim p`). Shape: [batch_size, ...].
69
+ step_size (Optional[float]): The step size. Must be None for adaptive step solvers.
70
+ method (str): A method supported by torchdiffeq. Defaults to "euler". Other commonly used solvers are "dopri5", "midpoint" and "heun3". For a complete list, see torchdiffeq.
71
+ atol (float): Absolute tolerance, used for adaptive step solvers.
72
+ rtol (float): Relative tolerance, used for adaptive step solvers.
73
+ time_grid (Tensor): The process is solved in the interval [min(time_grid, max(time_grid)] and if step_size is None then time discretization is set by the time grid. May specify a descending time_grid to solve in the reverse direction. Defaults to torch.tensor([0.0, 1.0]).
74
+ return_intermediates (bool, optional): If True then return intermediate time steps according to time_grid. Defaults to False.
75
+ enable_grad (bool, optional): Whether to compute gradients during sampling. Defaults to False.
76
+ **model_extras: Additional input for the model.
77
+
78
+ Returns:
79
+ Union[Tensor, Sequence[Tensor]]: The last timestep when return_intermediates=False, otherwise all values specified in time_grid.
80
+ """
81
+
82
+ time_grid = time_grid.to(x_init.device)
83
+
84
+ def ode_func(t, x):
85
+ return self.velocity_model(x=x, t=t, **model_extras)
86
+
87
+ ode_opts = {"step_size": step_size} if step_size is not None else {}
88
+
89
+ with torch.set_grad_enabled(enable_grad):
90
+ # Approximate ODE solution with numerical ODE solver
91
+ sol = odeint(
92
+ ode_func,
93
+ x_init,
94
+ time_grid,
95
+ method=method,
96
+ options=ode_opts,
97
+ atol=atol,
98
+ rtol=rtol,
99
+ )
100
+
101
+ if return_intermediates:
102
+ return sol
103
+ else:
104
+ return sol[-1]
105
+
106
+ def compute_likelihood(
107
+ self,
108
+ x_1: Tensor,
109
+ log_p0: Callable[[Tensor], Tensor],
110
+ step_size: Optional[float],
111
+ method: str = "euler",
112
+ atol: float = 1e-5,
113
+ rtol: float = 1e-5,
114
+ time_grid: Tensor = torch.tensor([1.0, 0.0]),
115
+ return_intermediates: bool = False,
116
+ exact_divergence: bool = False,
117
+ enable_grad: bool = False,
118
+ **model_extras,
119
+ ) -> Union[Tuple[Tensor, Tensor], Tuple[Sequence[Tensor], Tensor]]:
120
+ r"""Solve for log likelihood given a target sample at :math:`t=0`.
121
+
122
+ Works similarly to sample, but solves the ODE in reverse to compute the log-likelihood. The velocity model must be differentiable with respect to x.
123
+ The function assumes log_p0 is the log probability of the source distribution at :math:`t=0`.
124
+
125
+ Args:
126
+ x_1 (Tensor): target sample (e.g., samples :math:`X_1 \sim p_1`).
127
+ log_p0 (Callable[[Tensor], Tensor]): Log probability function of the source distribution.
128
+ step_size (Optional[float]): The step size. Must be None for adaptive step solvers.
129
+ method (str): A method supported by torchdiffeq. Defaults to "euler". Other commonly used solvers are "dopri5", "midpoint" and "heun3". For a complete list, see torchdiffeq.
130
+ atol (float): Absolute tolerance, used for adaptive step solvers.
131
+ rtol (float): Relative tolerance, used for adaptive step solvers.
132
+ time_grid (Tensor): If step_size is None then time discretization is set by the time grid. Must start at 1.0 and end at 0.0, otherwise the likelihood computation is not valid. Defaults to torch.tensor([1.0, 0.0]).
133
+ return_intermediates (bool, optional): If True then return intermediate time steps according to time_grid. Otherwise only return the final sample. Defaults to False.
134
+ exact_divergence (bool): Whether to compute the exact divergence or use the Hutchinson estimator.
135
+ enable_grad (bool, optional): Whether to compute gradients during sampling. Defaults to False.
136
+ **model_extras: Additional input for the model.
137
+
138
+ Returns:
139
+ Union[Tuple[Tensor, Tensor], Tuple[Sequence[Tensor], Tensor]]: Samples at time_grid and log likelihood values of given x_1.
140
+ """
141
+ assert (
142
+ time_grid[0] == 1.0 and time_grid[-1] == 0.0
143
+ ), f"Time grid must start at 1.0 and end at 0.0. Got {time_grid}"
144
+
145
+ # Fix the random projection for the Hutchinson divergence estimator
146
+ if not exact_divergence:
147
+ z = (torch.randn_like(x_1).to(x_1.device) < 0) * 2.0 - 1.0
148
+
149
+ def ode_func(x, t):
150
+ return self.velocity_model(x=x, t=t, **model_extras)
151
+
152
+ def dynamics_func(t, states):
153
+ xt = states[0]
154
+ with torch.set_grad_enabled(True):
155
+ xt.requires_grad_()
156
+ ut = ode_func(xt, t)
157
+
158
+ if exact_divergence:
159
+ # Compute exact divergence
160
+ div = 0
161
+ for i in range(ut.flatten(1).shape[1]):
162
+ div += gradient(ut[:, i], xt, create_graph=True)[:, i].detach()
163
+ else:
164
+ # Compute Hutchinson divergence estimator E[z^T D_x(ut) z]
165
+ ut_dot_z = torch.einsum(
166
+ "ij,ij->i", ut.flatten(start_dim=1), z.flatten(start_dim=1)
167
+ )
168
+ grad_ut_dot_z = gradient(ut_dot_z, xt)
169
+ div = torch.einsum(
170
+ "ij,ij->i",
171
+ grad_ut_dot_z.flatten(start_dim=1),
172
+ z.flatten(start_dim=1),
173
+ )
174
+
175
+ return ut.detach(), div.detach()
176
+
177
+ y_init = (x_1, torch.zeros(x_1.shape[0], device=x_1.device))
178
+ ode_opts = {"step_size": step_size} if step_size is not None else {}
179
+
180
+ with torch.set_grad_enabled(enable_grad):
181
+ sol, log_det = odeint(
182
+ dynamics_func,
183
+ y_init,
184
+ time_grid,
185
+ method=method,
186
+ options=ode_opts,
187
+ atol=atol,
188
+ rtol=rtol,
189
+ )
190
+
191
+ x_source = sol[-1]
192
+ source_log_p = log_p0(x_source)
193
+
194
+ if return_intermediates:
195
+ return sol, source_log_p + log_det[-1]
196
+ else:
197
+ return sol[-1], source_log_p + log_det[-1]
flow_matching/solver/riemannian_ode_solver.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import math
8
+ from typing import Callable
9
+
10
+ import torch
11
+ from torch import Tensor
12
+
13
+ from flow_matching.solver.solver import Solver
14
+ from flow_matching.utils import ModelWrapper
15
+ from flow_matching.utils.manifolds import geodesic, Manifold
16
+
17
+ try:
18
+ from tqdm import tqdm
19
+
20
+ TQDM_AVAILABLE = True
21
+ except ImportError:
22
+ TQDM_AVAILABLE = False
23
+
24
+
25
+ class RiemannianODESolver(Solver):
26
+ r"""Riemannian ODE solver
27
+ Initialize the ``RiemannianODESolver``.
28
+
29
+ Args:
30
+ manifold (Manifold): the manifold to solve on.
31
+ velocity_model (ModelWrapper): a velocity field model receiving :math:`(x,t)`
32
+ and returning :math:`u_t(x)` which is assumed to lie on the tangent plane at `x`.
33
+ """
34
+
35
+ def __init__(self, manifold: Manifold, velocity_model: ModelWrapper):
36
+ super().__init__()
37
+ self.manifold = manifold
38
+ self.velocity_model = velocity_model
39
+
40
+ def sample(
41
+ self,
42
+ x_init: Tensor,
43
+ step_size: float,
44
+ projx: bool = True,
45
+ proju: bool = True,
46
+ method: str = "euler",
47
+ time_grid: Tensor = torch.tensor([0.0, 1.0]),
48
+ return_intermediates: bool = False,
49
+ verbose: bool = False,
50
+ enable_grad: bool = False,
51
+ **model_extras,
52
+ ) -> Tensor:
53
+ r"""Solve the ODE with the `velocity_field` on the manifold.
54
+
55
+ Args:
56
+ x_init (Tensor): initial conditions (e.g., source samples :math:`X_0 \sim p`).
57
+ step_size (float): The step size.
58
+ projx (bool): Whether to project the point onto the manifold at each step. Defaults to True.
59
+ proju (bool): Whether to project the vector field onto the tangent plane at each step. Defaults to True.
60
+ method (str): One of ["euler", "midpoint", "rk4"]. Defaults to "euler".
61
+ time_grid (Tensor, optional): The process is solved in the interval [min(time_grid, max(time_grid)] and if step_size is None then time discretization is set by the time grid. Defaults to torch.tensor([0.0,1.0]).
62
+ return_intermediates (bool, optional): If True then return intermediate time steps according to time_grid. Defaults to False.
63
+ verbose (bool, optional): Whether to print progress bars. Defaults to False.
64
+ enable_grad (bool, optional): Whether to compute gradients during sampling. Defaults to False.
65
+ **model_extras: Additional input for the model.
66
+
67
+ Returns:
68
+ Tensor: The sampled sequence. Defaults to returning samples at :math:`t=1`.
69
+
70
+ Raises:
71
+ ImportError: To run in verbose mode, tqdm must be installed.
72
+ """
73
+ step_fns = {
74
+ "euler": _euler_step,
75
+ "midpoint": _midpoint_step,
76
+ "rk4": _rk4_step,
77
+ }
78
+ assert method in step_fns.keys(), f"Unknown method {method}"
79
+ step_fn = step_fns[method]
80
+
81
+ def velocity_func(x, t):
82
+ return self.velocity_model(x=x, t=t, **model_extras)
83
+
84
+ # --- Factor this out.
85
+ time_grid = torch.sort(time_grid.to(device=x_init.device)).values
86
+
87
+ if step_size is None:
88
+ # If step_size is None then set the t discretization to time_grid.
89
+ t_discretization = time_grid
90
+ n_steps = len(time_grid) - 1
91
+ else:
92
+ # If step_size is float then t discretization is uniform with step size set by step_size.
93
+ t_init = time_grid[0].item()
94
+ t_final = time_grid[-1].item()
95
+ assert (
96
+ t_final - t_init
97
+ ) > step_size, f"Time interval [min(time_grid), max(time_grid)] must be larger than step_size. Got a time interval [{t_init}, {t_final}] and step_size {step_size}."
98
+
99
+ n_steps = math.ceil((t_final - t_init) / step_size)
100
+ t_discretization = torch.tensor(
101
+ [step_size * i for i in range(n_steps)] + [t_final],
102
+ device=x_init.device,
103
+ )
104
+ # ---
105
+ t0s = t_discretization[:-1]
106
+
107
+ if verbose:
108
+ if not TQDM_AVAILABLE:
109
+ raise ImportError(
110
+ "tqdm is required for verbose mode. Please install it."
111
+ )
112
+ t0s = tqdm(t0s)
113
+
114
+ if return_intermediates:
115
+ xts = []
116
+ i_ret = 0
117
+
118
+ with torch.set_grad_enabled(enable_grad):
119
+ xt = x_init
120
+ for t0, t1 in zip(t0s, t_discretization[1:]):
121
+ dt = t1 - t0
122
+ xt_next = step_fn(
123
+ velocity_func,
124
+ xt,
125
+ t0,
126
+ dt,
127
+ manifold=self.manifold,
128
+ projx=projx,
129
+ proju=proju,
130
+ )
131
+ if return_intermediates:
132
+ while (
133
+ i_ret < len(time_grid)
134
+ and t0 <= time_grid[i_ret]
135
+ and time_grid[i_ret] <= t1
136
+ ):
137
+ xts.append(
138
+ interp(self.manifold, xt, xt_next, t0, t1, time_grid[i_ret])
139
+ )
140
+ i_ret += 1
141
+ xt = xt_next
142
+
143
+ if return_intermediates:
144
+ return torch.stack(xts, dim=0)
145
+ else:
146
+ return xt
147
+
148
+
149
+ def interp(manifold, xt, xt_next, t, t_next, t_ret):
150
+ return geodesic(manifold, xt, xt_next)(
151
+ (t_ret - t) / (t_next - t).reshape(1)
152
+ ).reshape_as(xt)
153
+
154
+
155
+ def _euler_step(
156
+ velocity_model: Callable,
157
+ xt: Tensor,
158
+ t0: Tensor,
159
+ dt: Tensor,
160
+ manifold: Manifold,
161
+ projx: bool = True,
162
+ proju: bool = True,
163
+ ) -> Tensor:
164
+ r"""Perform an Euler step on a manifold.
165
+
166
+ Args:
167
+ velocity_model (Callable): the velocity model
168
+ xt (Tensor): tensor containing the state at time t0
169
+ t0 (Tensor): the time at which this step is taken
170
+ dt (Tensor): the step size
171
+ manifold (Manifold): a manifold object
172
+ projx (bool, optional): whether to project the state onto the manifold. Defaults to True.
173
+ proju (bool, optional): whether to project the velocity onto the tangent plane. Defaults to True.
174
+
175
+ Returns:
176
+ Tensor: tensor containing the state after the step
177
+ """
178
+ velocity_fn = lambda x, t: (
179
+ manifold.proju(x, velocity_model(x, t)) if proju else velocity_model(x, t)
180
+ )
181
+ projx_fn = lambda x: manifold.projx(x) if projx else x
182
+
183
+ vt = velocity_fn(xt, t0)
184
+
185
+ xt = xt + dt * vt
186
+
187
+ return projx_fn(xt)
188
+
189
+
190
+ def _midpoint_step(
191
+ velocity_model: Callable,
192
+ xt: Tensor,
193
+ t0: Tensor,
194
+ dt: Tensor,
195
+ manifold: Manifold,
196
+ projx: bool = True,
197
+ proju: bool = True,
198
+ ) -> Tensor:
199
+ r"""Perform a midpoint step on a manifold.
200
+
201
+ Args:
202
+ velocity_model (Callable): the velocity model
203
+ xt (Tensor): tensor containing the state at time t0
204
+ t0 (Tensor): the time at which this step is taken
205
+ dt (Tensor): the step size
206
+ manifold (Manifold): a manifold object
207
+ projx (bool, optional): whether to project the state onto the manifold. Defaults to True.
208
+ proju (bool, optional): whether to project the velocity onto the tangent plane. Defaults to True.
209
+
210
+ Returns:
211
+ Tensor: tensor containing the state after the step
212
+ """
213
+ velocity_fn = lambda x, t: (
214
+ manifold.proju(x, velocity_model(x, t)) if proju else velocity_model(x, t)
215
+ )
216
+ projx_fn = lambda x: manifold.projx(x) if projx else x
217
+
218
+ half_dt = 0.5 * dt
219
+ vt = velocity_fn(xt, t0)
220
+ x_mid = xt + half_dt * vt
221
+ x_mid = projx_fn(x_mid)
222
+
223
+ xt = xt + dt * velocity_fn(x_mid, t0 + half_dt)
224
+
225
+ return projx_fn(xt)
226
+
227
+
228
+ def _rk4_step(
229
+ velocity_model: Callable,
230
+ xt: Tensor,
231
+ t0: Tensor,
232
+ dt: Tensor,
233
+ manifold: Manifold,
234
+ projx: bool = True,
235
+ proju: bool = True,
236
+ ) -> Tensor:
237
+ r"""Perform an RK4 step on a manifold.
238
+
239
+ Args:
240
+ velocity_model (Callable): the velocity model
241
+ xt (Tensor): tensor containing the state at time t0
242
+ t0 (Tensor): the time at which this step is taken
243
+ dt (Tensor): the step size
244
+ manifold (Manifold): a manifold object
245
+ projx (bool, optional): whether to project the state onto the manifold. Defaults to True.
246
+ proju (bool, optional): whether to project the velocity onto the tangent plane. Defaults to True.
247
+
248
+ Returns:
249
+ Tensor: tensor containing the state after the step
250
+ """
251
+ velocity_fn = lambda x, t: (
252
+ manifold.proju(x, velocity_model(x, t)) if proju else velocity_model(x, t)
253
+ )
254
+ projx_fn = lambda x: manifold.projx(x) if projx else x
255
+
256
+ k1 = velocity_fn(xt, t0)
257
+ k2 = velocity_fn(projx_fn(xt + dt * k1 / 3), t0 + dt / 3)
258
+ k3 = velocity_fn(projx_fn(xt + dt * (k2 - k1 / 3)), t0 + dt * 2 / 3)
259
+ k4 = velocity_fn(projx_fn(xt + dt * (k1 - k2 + k3)), t0 + dt)
260
+
261
+ return projx_fn(xt + (k1 + 3 * (k2 + k3) + k4) * dt * 0.125)
flow_matching/solver/solver.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from abc import ABC, abstractmethod
8
+
9
+ from torch import nn, Tensor
10
+
11
+
12
+ class Solver(ABC, nn.Module):
13
+ """Abstract base class for solvers."""
14
+
15
+ @abstractmethod
16
+ def sample(self, x_0: Tensor = None) -> Tensor:
17
+ ...
flow_matching/solver/utils.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import torch
8
+ from torch import Tensor
9
+
10
+
11
+ def get_nearest_times(time_grid: Tensor, t_discretization: Tensor) -> Tensor:
12
+ distances = torch.cdist(
13
+ time_grid.unsqueeze(1),
14
+ t_discretization.unsqueeze(1),
15
+ compute_mode="donot_use_mm_for_euclid_dist",
16
+ )
17
+ nearest_indices = distances.argmin(dim=1)
18
+
19
+ return t_discretization[nearest_indices]
flow_matching/utils/.DS_Store ADDED
Binary file (8.2 kB). View file
 
flow_matching/utils/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from .categorical_sampler import categorical
8
+ from .model_wrapper import ModelWrapper
9
+ from .utils import expand_tensor_like, gradient, unsqueeze_to_match
10
+
11
+ __all__ = [
12
+ "unsqueeze_to_match",
13
+ "expand_tensor_like",
14
+ "gradient",
15
+ "categorical",
16
+ "ModelWrapper",
17
+ ]
flow_matching/utils/categorical_sampler.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import torch
8
+ from torch import Tensor
9
+
10
+
11
+ def categorical(probs: Tensor) -> Tensor:
12
+ r"""Categorical sampler according to weights in the last dimension of ``probs`` using :func:`torch.multinomial`.
13
+
14
+ Args:
15
+ probs (Tensor): probabilities.
16
+
17
+ Returns:
18
+ Tensor: Samples.
19
+ """
20
+
21
+ return torch.multinomial(probs.flatten(0, -2), 1, replacement=True).view(
22
+ *probs.shape[:-1]
23
+ )
flow_matching/utils/manifolds/.DS_Store ADDED
Binary file (6.15 kB). View file
 
flow_matching/utils/manifolds/__init__.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ from .manifold import Euclidean, Manifold
8
+ from .sphere import Sphere
9
+ from .torus import FlatTorus
10
+ from .utils import geodesic
11
+
12
+ __all__ = [
13
+ "Euclidean",
14
+ "Manifold",
15
+ "Sphere",
16
+ "FlatTorus",
17
+ "geodesic",
18
+ ]
flow_matching/utils/manifolds/manifold.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the CC-by-NC license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import abc
8
+
9
+ import torch.nn as nn
10
+ from torch import Tensor
11
+
12
+
13
+ class Manifold(nn.Module, metaclass=abc.ABCMeta):
14
+ """A manifold class that contains projection operations and logarithm and exponential maps."""
15
+
16
+ @abc.abstractmethod
17
+ def expmap(self, x: Tensor, u: Tensor) -> Tensor:
18
+ r"""Computes exponential map :math:`\exp_x(u)`.
19
+
20
+ Args:
21
+ x (Tensor): point on the manifold
22
+ u (Tensor): tangent vector at point :math:`x`
23
+
24
+ Raises:
25
+ NotImplementedError: if not implemented
26
+
27
+ Returns:
28
+ Tensor: transported point
29
+ """
30
+ raise NotImplementedError
31
+
32
+ @abc.abstractmethod
33
+ def logmap(self, x: Tensor, y: Tensor) -> Tensor:
34
+ r"""Computes logarithmic map :math:`\log_x(y)`.
35
+
36
+ Args:
37
+ x (Tensor): point on the manifold
38
+ y (Tensor): point on the manifold
39
+
40
+ Raises:
41
+ NotImplementedError: if not implemented
42
+
43
+ Returns:
44
+ Tensor: tangent vector at point :math:`x`
45
+ """
46
+ raise NotImplementedError
47
+
48
+ @abc.abstractmethod
49
+ def projx(self, x: Tensor) -> Tensor:
50
+ """Project point :math:`x` on the manifold.
51
+
52
+ Args:
53
+ x (Tensor): point to be projected
54
+
55
+ Raises:
56
+ NotImplementedError: if not implemented
57
+
58
+ Returns:
59
+ Tensor: projected point on the manifold
60
+ """
61
+ raise NotImplementedError
62
+
63
+ @abc.abstractmethod
64
+ def proju(self, x: Tensor, u: Tensor) -> Tensor:
65
+ """Project vector :math:`u` on a tangent space for :math:`x`.
66
+
67
+ Args:
68
+ x (Tensor): point on the manifold
69
+ u (Tensor): vector to be projected
70
+
71
+ Raises:
72
+ NotImplementedError: if not implemented
73
+
74
+ Returns:
75
+ Tensor: projected tangent vector
76
+ """
77
+ raise NotImplementedError
78
+
79
+
80
+ class Euclidean(Manifold):
81
+ """The Euclidean manifold."""
82
+
83
+ def expmap(self, x: Tensor, u: Tensor) -> Tensor:
84
+ return x + u
85
+
86
+ def logmap(self, x: Tensor, y: Tensor) -> Tensor:
87
+ return y - x
88
+
89
+ def projx(self, x: Tensor) -> Tensor:
90
+ return x
91
+
92
+ def proju(self, x: Tensor, u: Tensor) -> Tensor:
93
+ return u