funcflows 0.1.2__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. funcflows-0.1.3/PKG-INFO +291 -0
  2. funcflows-0.1.3/README.md +265 -0
  3. funcflows-0.1.3/funcflows.egg-info/PKG-INFO +291 -0
  4. {funcflows-0.1.2 → funcflows-0.1.3}/pyproject.toml +2 -2
  5. funcflows-0.1.2/PKG-INFO +0 -43
  6. funcflows-0.1.2/README.md +0 -17
  7. funcflows-0.1.2/funcflows.egg-info/PKG-INFO +0 -43
  8. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/__init__.py +0 -0
  9. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/__init__.py +0 -0
  10. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/abstract_reference_measure.py +0 -0
  11. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/bases.py +0 -0
  12. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/base_measures/gaussian_reference_measure.py +0 -0
  13. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/diagnostics/__init__.py +0 -0
  14. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/diagnostics/coverage.py +0 -0
  15. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/diagnostics/importance.py +0 -0
  16. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/__init__.py +0 -0
  17. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/alpha_divergence.py +0 -0
  18. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/flow_matching.py +0 -0
  19. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/negative_logl.py +0 -0
  20. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/objectives/reverse_kl.py +0 -0
  21. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/samplers/__init__.py +0 -0
  22. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/samplers/latent_pcn.py +0 -0
  23. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/__init__.py +0 -0
  24. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/abstract_transformation.py +0 -0
  25. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/__init__.py +0 -0
  26. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/base_continuous.py +0 -0
  27. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/conditioners.py +0 -0
  28. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/grid_fields.py +0 -0
  29. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/continuous/vector_fields.py +0 -0
  30. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/layers/__init__.py +0 -0
  31. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/layers/base_discrete.py +0 -0
  32. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/transports/layers/layer_classes.py +0 -0
  33. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/utils/__init__.py +0 -0
  34. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/utils/gaussian_misfit.py +0 -0
  35. {funcflows-0.1.2 → funcflows-0.1.3}/FuncFlows/utils/train.py +0 -0
  36. {funcflows-0.1.2 → funcflows-0.1.3}/LICENSE +0 -0
  37. {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/SOURCES.txt +0 -0
  38. {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/dependency_links.txt +0 -0
  39. {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/requires.txt +0 -0
  40. {funcflows-0.1.2 → funcflows-0.1.3}/funcflows.egg-info/top_level.txt +0 -0
  41. {funcflows-0.1.2 → funcflows-0.1.3}/setup.cfg +0 -0
  42. {funcflows-0.1.2 → funcflows-0.1.3}/tests/test_basis.py +0 -0
  43. {funcflows-0.1.2 → funcflows-0.1.3}/tests/test_measures.py +0 -0
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: funcflows
3
+ Version: 0.1.3
4
+ Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
5
+ Author: Liam Pinchbeck
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/funcflows/
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: torch
17
+ Requires-Dist: torchvision
18
+ Requires-Dist: tqdm
19
+ Requires-Dist: scipy
20
+ Requires-Dist: matplotlib
21
+ Provides-Extra: ot
22
+ Requires-Dist: scipy; extra == "ot"
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest; extra == "test"
25
+ Dynamic: license-file
26
+
27
+ # FuncFlows
28
+
29
+ Normalizing flows and flow matching on function spaces.
30
+
31
+ A function is represented by its coefficients on a Laplacian eigenbasis (`CosineBasis` or
32
+ `FourierBasis`), the reference measure is a Gaussian on those coefficients, and a transport is a
33
+ neural ODE in coefficient space. The vector fields (`LinearField`, `MatrixField`) have closed-form
34
+ divergences, so the density of the transported measure relative to the
35
+ reference measure is exact — no Hutchinson estimators — which makes reverse-KL training,
36
+ likelihood training and importance reweighting usable at hundreds of modes.
37
+
38
+ ```bash
39
+ pip install funcflows # torch + tqdm
40
+ ```
41
+
42
+ `torch` is large; if you need a specific CUDA/MPS build install it first from pytorch.org.
43
+
44
+ `examples/quickstart.py` runs both patterns below end to end in a couple of minutes on CPU.
45
+
46
+ ## The pieces
47
+
48
+ | Piece | Role |
49
+ |---|---|
50
+ | `CosineBasis(M)`, `FourierBasis(M)` | `M` orthonormal basis functions on [0, 1]; `basis.evaluate(points)` → design matrix |
51
+ | `GaussianReferenceMeasure(basis, alpha, power)` or `(basis, variances=...)` | base measure N(0, diag σ²) on the coefficients; `.sample(n)`, `.scale` (= σ) |
52
+ | `LinearField` | per-mode gain plus a data-dependent drift; exact for the linear-Gaussian part |
53
+ | `MatrixField(conditioner)` | one wide tanh layer over all coefficients; global nonlinear mixing |
54
+ | `SumField(*fields)` | add fields; traces add |
55
+ | `ContinuousTransformation(measure, field, num_steps)` | the flow: `.transport(v0)`, `.push_forward`, `.pull_back`, `.log_rn_at(v)` |
56
+ | `FlowMatching`, `ConditionalFlowMatching`, `ReverseKL`, `NegativeLogL` | objectives; each is a callable returning a loss |
57
+ | `train(objective, params, num_steps, learning_rate)` | Adam loop, returns the loss history |
58
+ | `ImportanceCorrection`, `coverage_curve`, `latent_pcn` | diagnostics and an exact latent-space MCMC sampler |
59
+
60
+ Every field takes `mode_scale=measure.scale`: the nonlinearity sees whitened coefficients and the
61
+ divergence is unchanged (similarity transform). Always pass it.
62
+
63
+ ## 1. Learn a prior from samples (flow matching)
64
+
65
+ ```python
66
+ import torch
67
+ from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
68
+ from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
69
+ MatrixField, TimeBasisConditioner)
70
+ from FuncFlows.objectives import FlowMatching
71
+ from FuncFlows.utils.train import train
72
+
73
+ M = 64
74
+ basis = CosineBasis(M)
75
+ coeffs = ... # [N, M] training functions projected onto the basis
76
+ measure = GaussianReferenceMeasure(basis, variances=coeffs.var(0)) # diagonal Gaussian fit = the GP baseline
77
+
78
+ field = SumField(
79
+ LinearField(M, num_time_modes=4),
80
+ MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=measure.scale))
81
+ flow = ContinuousTransformation(measure, field, num_steps=16)
82
+
83
+ losses = train(FlowMatching(flow, coeffs, batch_size=256, weights=1 / measure.scale, coupling="optimal"),
84
+ flow.parameters(), num_steps=10000, learning_rate=2e-3)
85
+
86
+ draws = flow.transport(measure.sample(1000)) # [1000, M] new functions
87
+ points = torch.linspace(0, 1, 400).reshape(-1, 1)
88
+ values = draws @ basis.evaluate(points).T # [1000, 400] on a grid
89
+ log_q = flow.log_rn_at(draws) # exact log density w.r.t. the reference measure
90
+ ```
91
+
92
+ `weights=1/measure.scale` whitens the loss so high modes count; `coupling="optimal"` needs
93
+ scipy and only makes sense for an unconditional flow.
94
+
95
+ ## 2. Posterior from a likelihood (no samples needed)
96
+
97
+ ```python
98
+ from FuncFlows.objectives import ReverseKL
99
+ from FuncFlows.utils.gaussian_misfit import GaussianMisfit
100
+ from FuncFlows.diagnostics import ImportanceCorrection
101
+
102
+ prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0) # Matérn-like spectrum
103
+ phi_obs = basis.evaluate(obs_points) # [n_obs, M]
104
+ forward = lambda v: (v @ phi_obs.T) ** 2 # any differentiable map
105
+ potential = GaussianMisfit(forward, data, noise_std) # -log likelihood, [batch] -> [batch]
106
+
107
+ flow = ContinuousTransformation(prior, field, num_steps=16) # same field recipe as above
108
+ train(ReverseKL(flow, potential, num_samples=64, path_gradient=True), flow.parameters(), num_steps=3000)
109
+
110
+ draws = flow.transport(prior.sample(2000))
111
+ check = ImportanceCorrection(flow, draws, potential) # exact because the traces are exact
112
+ print(check.efficiency, check.log_evidence) # efficiency near 1 = posterior matched
113
+ ```
114
+
115
+ For an exact sampler on top of a learned prior use `latent_pcn(flow, potential)`: pCN in the flow's
116
+ latent space, whose acceptance needs no Jacobian.
117
+
118
+ ## 3. Amortised posterior (condition on data)
119
+
120
+ ```python
121
+ from FuncFlows.transports.continuous import DataConditioner
122
+ from FuncFlows.objectives import ConditionalFlowMatching
123
+
124
+ C = ... # context dimension (e.g. whitened projected data)
125
+ field = SumField(
126
+ LinearField(M, C, num_time_modes=4, mode_scale=measure.scale), # data drift: the linear-Gaussian mean
127
+ MatrixField(DataConditioner(M, 256, C, num_time_modes=4), mode_scale=measure.scale))
128
+ flow = ContinuousTransformation(measure, field, num_steps=16)
129
+
130
+ def simulate(batch): # -> (target coeffs [batch, M], context [batch, C])
131
+ ...
132
+
133
+ train(ConditionalFlowMatching(flow, simulate, measure.sample, batch_size=256, weights=1 / measure.scale),
134
+ flow.parameters(), num_steps=20000, learning_rate=2e-3)
135
+
136
+ posterior = flow.transport(measure.sample(1000), context.expand(1000, -1)) # one ODE solve per posterior
137
+ ```
138
+
139
+ Whiten the context by `measure.scale` when it is itself a projected function; otherwise the drift
140
+ weights for high modes have to reach 1/σ and never do. When a conjugate (linear-Gaussian)
141
+ posterior is available in closed form, use it as the base measure and let the flow learn the residual.
142
+
143
+ ## Diagnostics
144
+
145
+ ```python
146
+ from FuncFlows.diagnostics import coverage_curve, coverage_error
147
+ levels, coverage = coverage_curve(samples, truths, weights=1 / measure.scale) # TARP; samples [cases, draws, M]
148
+ coverage_error(levels, coverage) # signed max gap from the diagonal: negative = overconfident
149
+ ```
150
+
151
+ ## Layout
152
+
153
+ ```
154
+ FuncFlows/
155
+ base_measures/ bases, GaussianReferenceMeasure
156
+ transports/ continuous/: fields, conditioners, ContinuousTransformation
157
+ (grid_fields.py: PointwiseField / OperatorField, spatial layers — experimental)
158
+ objectives/ flow_matching, reverse_kl, negative_logl
159
+ samplers/ latent_pcn
160
+ diagnostics/ importance, coverage
161
+ utils/ train, gaussian_misfit
162
+ ```
163
+
164
+
165
+
166
+ # Full Example
167
+
168
+
169
+ ```python
170
+ import matplotlib
171
+ matplotlib.use("Agg")
172
+ import matplotlib.pyplot as plt
173
+ import torch
174
+
175
+ from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
176
+ from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
177
+ MatrixField, TimeBasisConditioner)
178
+ from FuncFlows.objectives import ReverseKL, FlowMatching
179
+ from FuncFlows.samplers import latent_pcn
180
+ from FuncFlows.utils.train import train
181
+ from FuncFlows.utils.gaussian_misfit import GaussianMisfit
182
+ from FuncFlows.diagnostics import ImportanceCorrection
183
+
184
+ torch.manual_seed(1)
185
+ M, NUM_OBS, NOISE, N = 32, 15, 0.05, 2000
186
+ basis = CosineBasis(M)
187
+ prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0)
188
+ x = torch.linspace(0, 1, 400, dtype=torch.float64)[:, None]
189
+ design = basis.evaluate(x)
190
+
191
+ # data || y = f(x)^2 + noise at NUM_OBS random points
192
+ truth = prior.sample(1)
193
+ obs_x = torch.rand(NUM_OBS, 1, dtype=torch.float64)
194
+ phi_obs = basis.evaluate(obs_x)
195
+ forward = lambda v: (v @ phi_obs.T) ** 2
196
+ data = forward(truth)[0] + NOISE * torch.randn(NUM_OBS, dtype=torch.float64)
197
+ misfit = GaussianMisfit(forward, data, NOISE) # -logl
198
+
199
+
200
+ def make_flow():
201
+ field = SumField(LinearField(M, num_time_modes=4),
202
+ MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=prior.scale))
203
+ return ContinuousTransformation(prior, field, num_steps=16)
204
+
205
+
206
+ @torch.no_grad()
207
+ def pcn(potential, num_chains=64, num_steps=20000, beta=0.15, burn=5000, thin=20):
208
+ state, keep = prior.sample(num_chains), []
209
+ energy = potential(state)
210
+ for step in range(num_steps):
211
+ proposal = (1 - beta ** 2) ** 0.5 * state + beta * prior.sample(num_chains)
212
+ proposal_energy = potential(proposal)
213
+ accept = torch.log(torch.rand_like(energy)) < energy - proposal_energy
214
+ state = torch.where(accept[:, None], proposal, state)
215
+ energy = torch.where(accept, proposal_energy, energy)
216
+ if step >= burn and step % thin == 0:
217
+ keep.append(state.clone())
218
+ return torch.cat(keep)
219
+
220
+
221
+ def laplace_gp():
222
+ white = torch.zeros(M, dtype=torch.float64, requires_grad=True)
223
+ opt = torch.optim.LBFGS([white], max_iter=200, line_search_fn="strong_wolfe")
224
+
225
+ def closure():
226
+ opt.zero_grad()
227
+ loss = misfit(white * prior.scale) + 0.5 * (white ** 2).sum()
228
+ loss.backward()
229
+ return loss
230
+
231
+ opt.step(closure)
232
+ with torch.no_grad():
233
+ jac = 2 * (phi_obs @ (white * prior.scale))[:, None] * phi_obs * prior.scale
234
+ cov = torch.linalg.inv(torch.eye(M, dtype=torch.float64) + jac.T @ jac / NOISE ** 2)
235
+ return (white + torch.randn(N, M, dtype=torch.float64) @ torch.linalg.cholesky(cov).T) * prior.scale
236
+
237
+
238
+ # run everything
239
+ results = {}
240
+ results["pCN reference"] = pcn(misfit)[torch.randperm(64 * 750)[:N]] # thin to N for the comparisons
241
+
242
+ results["GP (Laplace)"] = laplace_gp()
243
+
244
+ rkl = make_flow()
245
+ train(ReverseKL(rkl, misfit, num_samples=64, path_gradient=True), rkl.parameters(), num_steps=2500, learning_rate=3e-3)
246
+ with torch.no_grad():
247
+ results["reverse KL"] = rkl.transport(prior.sample(N))
248
+
249
+ fm = make_flow()
250
+ train(FlowMatching(fm, results["pCN reference"], batch_size=256), fm.parameters(), num_steps=4000, learning_rate=3e-3)
251
+ with torch.no_grad():
252
+ results["flow matching"] = fm.transport(prior.sample(N))
253
+
254
+ # pCN in the latent space of the posterior flow: target exp(-Phi(Tz)) prior(Tz)/q(Tz) mu0(dz), so add the
255
+ # flow's exact log-density to the potential. A good flow makes the target nearly flat -> big steps accepted.
256
+ corrected = lambda v: misfit(v) + fm.log_rn_at(v) # noqa: E731
257
+ draws, info = latent_pcn(fm, corrected, num_chains=128, num_steps=1500, beta=0.5, thin=10)
258
+ results["latent pCN"] = draws[torch.randperm(len(draws))[:N]]
259
+
260
+
261
+
262
+ # report
263
+ ref = results["pCN reference"] @ design.T
264
+ print(f"\n{'method':16s} {'+sign':>6s} {'E-dist':>8s} (ideal 0.50; distance to pCN)")
265
+ for name, v in results.items():
266
+ values = v @ design.T
267
+ sign = ((v @ truth[0]) > 0).double().mean().item()
268
+ dist = (2 * torch.cdist(values, ref).mean() - torch.cdist(values, values).mean() - torch.cdist(ref, ref).mean()) / 20
269
+ print(f"{name:16s} {sign:6.2f} {dist.item():8.4f}")
270
+ for name, flow in [("reverse KL", rkl), ("flow matching", fm)]:
271
+ with torch.no_grad():
272
+ c = ImportanceCorrection(flow, results[name], misfit)
273
+ print(f"{name}: importance efficiency {c.efficiency:.3f}, log evidence {c.log_evidence:.2f}")
274
+ print(f"latent pCN acceptance {info['acceptance']:.2f} at beta 0.5 (plain pCN would be ~0)")
275
+
276
+
277
+ # figures
278
+ fig, axes = plt.subplots(1, len(results), figsize=(3.6 * len(results), 3.2), sharey=True)
279
+ tv = (truth @ design.T)[0]
280
+ for ax, (name, v) in zip(axes, results.items()):
281
+ ax.plot(x[:, 0], (v[:40] @ design.T).T, color="C0", alpha=0.15, lw=1)
282
+ ax.plot(x[:, 0], tv, "k", lw=1.5)
283
+ ax.plot(x[:, 0], -tv, "k--", lw=1.5)
284
+ ax.scatter(obs_x[:, 0], data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
285
+ ax.scatter(obs_x[:, 0], -data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
286
+ ax.set_title(name, fontsize=10)
287
+ fig.suptitle("y = f(x)^2 + noise: black = ±truth, red = ±sqrt(data), blue = posterior draws", fontsize=10)
288
+ fig.tight_layout()
289
+ fig.savefig("bimodal_posterior.png", dpi=130)
290
+ print("saved bimodal_posterior.png")
291
+ ```
@@ -0,0 +1,265 @@
1
+ # FuncFlows
2
+
3
+ Normalizing flows and flow matching on function spaces.
4
+
5
+ A function is represented by its coefficients on a Laplacian eigenbasis (`CosineBasis` or
6
+ `FourierBasis`), the reference measure is a Gaussian on those coefficients, and a transport is a
7
+ neural ODE in coefficient space. The vector fields (`LinearField`, `MatrixField`) have closed-form
8
+ divergences, so the density of the transported measure relative to the
9
+ reference measure is exact — no Hutchinson estimators — which makes reverse-KL training,
10
+ likelihood training and importance reweighting usable at hundreds of modes.
11
+
12
+ ```bash
13
+ pip install funcflows # torch + tqdm
14
+ ```
15
+
16
+ `torch` is large; if you need a specific CUDA/MPS build install it first from pytorch.org.
17
+
18
+ `examples/quickstart.py` runs both patterns below end to end in a couple of minutes on CPU.
19
+
20
+ ## The pieces
21
+
22
+ | Piece | Role |
23
+ |---|---|
24
+ | `CosineBasis(M)`, `FourierBasis(M)` | `M` orthonormal basis functions on [0, 1]; `basis.evaluate(points)` → design matrix |
25
+ | `GaussianReferenceMeasure(basis, alpha, power)` or `(basis, variances=...)` | base measure N(0, diag σ²) on the coefficients; `.sample(n)`, `.scale` (= σ) |
26
+ | `LinearField` | per-mode gain plus a data-dependent drift; exact for the linear-Gaussian part |
27
+ | `MatrixField(conditioner)` | one wide tanh layer over all coefficients; global nonlinear mixing |
28
+ | `SumField(*fields)` | add fields; traces add |
29
+ | `ContinuousTransformation(measure, field, num_steps)` | the flow: `.transport(v0)`, `.push_forward`, `.pull_back`, `.log_rn_at(v)` |
30
+ | `FlowMatching`, `ConditionalFlowMatching`, `ReverseKL`, `NegativeLogL` | objectives; each is a callable returning a loss |
31
+ | `train(objective, params, num_steps, learning_rate)` | Adam loop, returns the loss history |
32
+ | `ImportanceCorrection`, `coverage_curve`, `latent_pcn` | diagnostics and an exact latent-space MCMC sampler |
33
+
34
+ Every field takes `mode_scale=measure.scale`: the nonlinearity sees whitened coefficients and the
35
+ divergence is unchanged (similarity transform). Always pass it.
36
+
37
+ ## 1. Learn a prior from samples (flow matching)
38
+
39
+ ```python
40
+ import torch
41
+ from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
42
+ from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
43
+ MatrixField, TimeBasisConditioner)
44
+ from FuncFlows.objectives import FlowMatching
45
+ from FuncFlows.utils.train import train
46
+
47
+ M = 64
48
+ basis = CosineBasis(M)
49
+ coeffs = ... # [N, M] training functions projected onto the basis
50
+ measure = GaussianReferenceMeasure(basis, variances=coeffs.var(0)) # diagonal Gaussian fit = the GP baseline
51
+
52
+ field = SumField(
53
+ LinearField(M, num_time_modes=4),
54
+ MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=measure.scale))
55
+ flow = ContinuousTransformation(measure, field, num_steps=16)
56
+
57
+ losses = train(FlowMatching(flow, coeffs, batch_size=256, weights=1 / measure.scale, coupling="optimal"),
58
+ flow.parameters(), num_steps=10000, learning_rate=2e-3)
59
+
60
+ draws = flow.transport(measure.sample(1000)) # [1000, M] new functions
61
+ points = torch.linspace(0, 1, 400).reshape(-1, 1)
62
+ values = draws @ basis.evaluate(points).T # [1000, 400] on a grid
63
+ log_q = flow.log_rn_at(draws) # exact log density w.r.t. the reference measure
64
+ ```
65
+
66
+ `weights=1/measure.scale` whitens the loss so high modes count; `coupling="optimal"` needs
67
+ scipy and only makes sense for an unconditional flow.
68
+
69
+ ## 2. Posterior from a likelihood (no samples needed)
70
+
71
+ ```python
72
+ from FuncFlows.objectives import ReverseKL
73
+ from FuncFlows.utils.gaussian_misfit import GaussianMisfit
74
+ from FuncFlows.diagnostics import ImportanceCorrection
75
+
76
+ prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0) # Matérn-like spectrum
77
+ phi_obs = basis.evaluate(obs_points) # [n_obs, M]
78
+ forward = lambda v: (v @ phi_obs.T) ** 2 # any differentiable map
79
+ potential = GaussianMisfit(forward, data, noise_std) # -log likelihood, [batch] -> [batch]
80
+
81
+ flow = ContinuousTransformation(prior, field, num_steps=16) # same field recipe as above
82
+ train(ReverseKL(flow, potential, num_samples=64, path_gradient=True), flow.parameters(), num_steps=3000)
83
+
84
+ draws = flow.transport(prior.sample(2000))
85
+ check = ImportanceCorrection(flow, draws, potential) # exact because the traces are exact
86
+ print(check.efficiency, check.log_evidence) # efficiency near 1 = posterior matched
87
+ ```
88
+
89
+ For an exact sampler on top of a learned prior use `latent_pcn(flow, potential)`: pCN in the flow's
90
+ latent space, whose acceptance needs no Jacobian.
91
+
92
+ ## 3. Amortised posterior (condition on data)
93
+
94
+ ```python
95
+ from FuncFlows.transports.continuous import DataConditioner
96
+ from FuncFlows.objectives import ConditionalFlowMatching
97
+
98
+ C = ... # context dimension (e.g. whitened projected data)
99
+ field = SumField(
100
+ LinearField(M, C, num_time_modes=4, mode_scale=measure.scale), # data drift: the linear-Gaussian mean
101
+ MatrixField(DataConditioner(M, 256, C, num_time_modes=4), mode_scale=measure.scale))
102
+ flow = ContinuousTransformation(measure, field, num_steps=16)
103
+
104
+ def simulate(batch): # -> (target coeffs [batch, M], context [batch, C])
105
+ ...
106
+
107
+ train(ConditionalFlowMatching(flow, simulate, measure.sample, batch_size=256, weights=1 / measure.scale),
108
+ flow.parameters(), num_steps=20000, learning_rate=2e-3)
109
+
110
+ posterior = flow.transport(measure.sample(1000), context.expand(1000, -1)) # one ODE solve per posterior
111
+ ```
112
+
113
+ Whiten the context by `measure.scale` when it is itself a projected function; otherwise the drift
114
+ weights for high modes have to reach 1/σ and never do. When a conjugate (linear-Gaussian)
115
+ posterior is available in closed form, use it as the base measure and let the flow learn the residual.
116
+
117
+ ## Diagnostics
118
+
119
+ ```python
120
+ from FuncFlows.diagnostics import coverage_curve, coverage_error
121
+ levels, coverage = coverage_curve(samples, truths, weights=1 / measure.scale) # TARP; samples [cases, draws, M]
122
+ coverage_error(levels, coverage) # signed max gap from the diagonal: negative = overconfident
123
+ ```
124
+
125
+ ## Layout
126
+
127
+ ```
128
+ FuncFlows/
129
+ base_measures/ bases, GaussianReferenceMeasure
130
+ transports/ continuous/: fields, conditioners, ContinuousTransformation
131
+ (grid_fields.py: PointwiseField / OperatorField, spatial layers — experimental)
132
+ objectives/ flow_matching, reverse_kl, negative_logl
133
+ samplers/ latent_pcn
134
+ diagnostics/ importance, coverage
135
+ utils/ train, gaussian_misfit
136
+ ```
137
+
138
+
139
+
140
+ # Full Example
141
+
142
+
143
+ ```python
144
+ import matplotlib
145
+ matplotlib.use("Agg")
146
+ import matplotlib.pyplot as plt
147
+ import torch
148
+
149
+ from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
150
+ from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
151
+ MatrixField, TimeBasisConditioner)
152
+ from FuncFlows.objectives import ReverseKL, FlowMatching
153
+ from FuncFlows.samplers import latent_pcn
154
+ from FuncFlows.utils.train import train
155
+ from FuncFlows.utils.gaussian_misfit import GaussianMisfit
156
+ from FuncFlows.diagnostics import ImportanceCorrection
157
+
158
+ torch.manual_seed(1)
159
+ M, NUM_OBS, NOISE, N = 32, 15, 0.05, 2000
160
+ basis = CosineBasis(M)
161
+ prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0)
162
+ x = torch.linspace(0, 1, 400, dtype=torch.float64)[:, None]
163
+ design = basis.evaluate(x)
164
+
165
+ # data || y = f(x)^2 + noise at NUM_OBS random points
166
+ truth = prior.sample(1)
167
+ obs_x = torch.rand(NUM_OBS, 1, dtype=torch.float64)
168
+ phi_obs = basis.evaluate(obs_x)
169
+ forward = lambda v: (v @ phi_obs.T) ** 2
170
+ data = forward(truth)[0] + NOISE * torch.randn(NUM_OBS, dtype=torch.float64)
171
+ misfit = GaussianMisfit(forward, data, NOISE) # -logl
172
+
173
+
174
+ def make_flow():
175
+ field = SumField(LinearField(M, num_time_modes=4),
176
+ MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=prior.scale))
177
+ return ContinuousTransformation(prior, field, num_steps=16)
178
+
179
+
180
+ @torch.no_grad()
181
+ def pcn(potential, num_chains=64, num_steps=20000, beta=0.15, burn=5000, thin=20):
182
+ state, keep = prior.sample(num_chains), []
183
+ energy = potential(state)
184
+ for step in range(num_steps):
185
+ proposal = (1 - beta ** 2) ** 0.5 * state + beta * prior.sample(num_chains)
186
+ proposal_energy = potential(proposal)
187
+ accept = torch.log(torch.rand_like(energy)) < energy - proposal_energy
188
+ state = torch.where(accept[:, None], proposal, state)
189
+ energy = torch.where(accept, proposal_energy, energy)
190
+ if step >= burn and step % thin == 0:
191
+ keep.append(state.clone())
192
+ return torch.cat(keep)
193
+
194
+
195
+ def laplace_gp():
196
+ white = torch.zeros(M, dtype=torch.float64, requires_grad=True)
197
+ opt = torch.optim.LBFGS([white], max_iter=200, line_search_fn="strong_wolfe")
198
+
199
+ def closure():
200
+ opt.zero_grad()
201
+ loss = misfit(white * prior.scale) + 0.5 * (white ** 2).sum()
202
+ loss.backward()
203
+ return loss
204
+
205
+ opt.step(closure)
206
+ with torch.no_grad():
207
+ jac = 2 * (phi_obs @ (white * prior.scale))[:, None] * phi_obs * prior.scale
208
+ cov = torch.linalg.inv(torch.eye(M, dtype=torch.float64) + jac.T @ jac / NOISE ** 2)
209
+ return (white + torch.randn(N, M, dtype=torch.float64) @ torch.linalg.cholesky(cov).T) * prior.scale
210
+
211
+
212
+ # run everything
213
+ results = {}
214
+ results["pCN reference"] = pcn(misfit)[torch.randperm(64 * 750)[:N]] # thin to N for the comparisons
215
+
216
+ results["GP (Laplace)"] = laplace_gp()
217
+
218
+ rkl = make_flow()
219
+ train(ReverseKL(rkl, misfit, num_samples=64, path_gradient=True), rkl.parameters(), num_steps=2500, learning_rate=3e-3)
220
+ with torch.no_grad():
221
+ results["reverse KL"] = rkl.transport(prior.sample(N))
222
+
223
+ fm = make_flow()
224
+ train(FlowMatching(fm, results["pCN reference"], batch_size=256), fm.parameters(), num_steps=4000, learning_rate=3e-3)
225
+ with torch.no_grad():
226
+ results["flow matching"] = fm.transport(prior.sample(N))
227
+
228
+ # pCN in the latent space of the posterior flow: target exp(-Phi(Tz)) prior(Tz)/q(Tz) mu0(dz), so add the
229
+ # flow's exact log-density to the potential. A good flow makes the target nearly flat -> big steps accepted.
230
+ corrected = lambda v: misfit(v) + fm.log_rn_at(v) # noqa: E731
231
+ draws, info = latent_pcn(fm, corrected, num_chains=128, num_steps=1500, beta=0.5, thin=10)
232
+ results["latent pCN"] = draws[torch.randperm(len(draws))[:N]]
233
+
234
+
235
+
236
+ # report
237
+ ref = results["pCN reference"] @ design.T
238
+ print(f"\n{'method':16s} {'+sign':>6s} {'E-dist':>8s} (ideal 0.50; distance to pCN)")
239
+ for name, v in results.items():
240
+ values = v @ design.T
241
+ sign = ((v @ truth[0]) > 0).double().mean().item()
242
+ dist = (2 * torch.cdist(values, ref).mean() - torch.cdist(values, values).mean() - torch.cdist(ref, ref).mean()) / 20
243
+ print(f"{name:16s} {sign:6.2f} {dist.item():8.4f}")
244
+ for name, flow in [("reverse KL", rkl), ("flow matching", fm)]:
245
+ with torch.no_grad():
246
+ c = ImportanceCorrection(flow, results[name], misfit)
247
+ print(f"{name}: importance efficiency {c.efficiency:.3f}, log evidence {c.log_evidence:.2f}")
248
+ print(f"latent pCN acceptance {info['acceptance']:.2f} at beta 0.5 (plain pCN would be ~0)")
249
+
250
+
251
+ # figures
252
+ fig, axes = plt.subplots(1, len(results), figsize=(3.6 * len(results), 3.2), sharey=True)
253
+ tv = (truth @ design.T)[0]
254
+ for ax, (name, v) in zip(axes, results.items()):
255
+ ax.plot(x[:, 0], (v[:40] @ design.T).T, color="C0", alpha=0.15, lw=1)
256
+ ax.plot(x[:, 0], tv, "k", lw=1.5)
257
+ ax.plot(x[:, 0], -tv, "k--", lw=1.5)
258
+ ax.scatter(obs_x[:, 0], data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
259
+ ax.scatter(obs_x[:, 0], -data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
260
+ ax.set_title(name, fontsize=10)
261
+ fig.suptitle("y = f(x)^2 + noise: black = ±truth, red = ±sqrt(data), blue = posterior draws", fontsize=10)
262
+ fig.tight_layout()
263
+ fig.savefig("bimodal_posterior.png", dpi=130)
264
+ print("saved bimodal_posterior.png")
265
+ ```
@@ -0,0 +1,291 @@
1
+ Metadata-Version: 2.4
2
+ Name: funcflows
3
+ Version: 0.1.3
4
+ Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
5
+ Author: Liam Pinchbeck
6
+ License: MIT
7
+ Project-URL: Homepage, https://pypi.org/project/funcflows/
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Science/Research
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: torch
17
+ Requires-Dist: torchvision
18
+ Requires-Dist: tqdm
19
+ Requires-Dist: scipy
20
+ Requires-Dist: matplotlib
21
+ Provides-Extra: ot
22
+ Requires-Dist: scipy; extra == "ot"
23
+ Provides-Extra: test
24
+ Requires-Dist: pytest; extra == "test"
25
+ Dynamic: license-file
26
+
27
+ # FuncFlows
28
+
29
+ Normalizing flows and flow matching on function spaces.
30
+
31
+ A function is represented by its coefficients on a Laplacian eigenbasis (`CosineBasis` or
32
+ `FourierBasis`), the reference measure is a Gaussian on those coefficients, and a transport is a
33
+ neural ODE in coefficient space. The vector fields (`LinearField`, `MatrixField`) have closed-form
34
+ divergences, so the density of the transported measure relative to the
35
+ reference measure is exact — no Hutchinson estimators — which makes reverse-KL training,
36
+ likelihood training and importance reweighting usable at hundreds of modes.
37
+
38
+ ```bash
39
+ pip install funcflows # torch + tqdm
40
+ ```
41
+
42
+ `torch` is large; if you need a specific CUDA/MPS build install it first from pytorch.org.
43
+
44
+ `examples/quickstart.py` runs both patterns below end to end in a couple of minutes on CPU.
45
+
46
+ ## The pieces
47
+
48
+ | Piece | Role |
49
+ |---|---|
50
+ | `CosineBasis(M)`, `FourierBasis(M)` | `M` orthonormal basis functions on [0, 1]; `basis.evaluate(points)` → design matrix |
51
+ | `GaussianReferenceMeasure(basis, alpha, power)` or `(basis, variances=...)` | base measure N(0, diag σ²) on the coefficients; `.sample(n)`, `.scale` (= σ) |
52
+ | `LinearField` | per-mode gain plus a data-dependent drift; exact for the linear-Gaussian part |
53
+ | `MatrixField(conditioner)` | one wide tanh layer over all coefficients; global nonlinear mixing |
54
+ | `SumField(*fields)` | add fields; traces add |
55
+ | `ContinuousTransformation(measure, field, num_steps)` | the flow: `.transport(v0)`, `.push_forward`, `.pull_back`, `.log_rn_at(v)` |
56
+ | `FlowMatching`, `ConditionalFlowMatching`, `ReverseKL`, `NegativeLogL` | objectives; each is a callable returning a loss |
57
+ | `train(objective, params, num_steps, learning_rate)` | Adam loop, returns the loss history |
58
+ | `ImportanceCorrection`, `coverage_curve`, `latent_pcn` | diagnostics and an exact latent-space MCMC sampler |
59
+
60
+ Every field takes `mode_scale=measure.scale`: the nonlinearity sees whitened coefficients and the
61
+ divergence is unchanged (similarity transform). Always pass it.
62
+
63
+ ## 1. Learn a prior from samples (flow matching)
64
+
65
+ ```python
66
+ import torch
67
+ from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
68
+ from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
69
+ MatrixField, TimeBasisConditioner)
70
+ from FuncFlows.objectives import FlowMatching
71
+ from FuncFlows.utils.train import train
72
+
73
+ M = 64
74
+ basis = CosineBasis(M)
75
+ coeffs = ... # [N, M] training functions projected onto the basis
76
+ measure = GaussianReferenceMeasure(basis, variances=coeffs.var(0)) # diagonal Gaussian fit = the GP baseline
77
+
78
+ field = SumField(
79
+ LinearField(M, num_time_modes=4),
80
+ MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=measure.scale))
81
+ flow = ContinuousTransformation(measure, field, num_steps=16)
82
+
83
+ losses = train(FlowMatching(flow, coeffs, batch_size=256, weights=1 / measure.scale, coupling="optimal"),
84
+ flow.parameters(), num_steps=10000, learning_rate=2e-3)
85
+
86
+ draws = flow.transport(measure.sample(1000)) # [1000, M] new functions
87
+ points = torch.linspace(0, 1, 400).reshape(-1, 1)
88
+ values = draws @ basis.evaluate(points).T # [1000, 400] on a grid
89
+ log_q = flow.log_rn_at(draws) # exact log density w.r.t. the reference measure
90
+ ```
91
+
92
+ `weights=1/measure.scale` whitens the loss so high modes count; `coupling="optimal"` needs
93
+ scipy and only makes sense for an unconditional flow.
94
+
95
+ ## 2. Posterior from a likelihood (no samples needed)
96
+
97
+ ```python
98
+ from FuncFlows.objectives import ReverseKL
99
+ from FuncFlows.utils.gaussian_misfit import GaussianMisfit
100
+ from FuncFlows.diagnostics import ImportanceCorrection
101
+
102
+ prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0) # Matérn-like spectrum
103
+ phi_obs = basis.evaluate(obs_points) # [n_obs, M]
104
+ forward = lambda v: (v @ phi_obs.T) ** 2 # any differentiable map
105
+ potential = GaussianMisfit(forward, data, noise_std) # -log likelihood, [batch] -> [batch]
106
+
107
+ flow = ContinuousTransformation(prior, field, num_steps=16) # same field recipe as above
108
+ train(ReverseKL(flow, potential, num_samples=64, path_gradient=True), flow.parameters(), num_steps=3000)
109
+
110
+ draws = flow.transport(prior.sample(2000))
111
+ check = ImportanceCorrection(flow, draws, potential) # exact because the traces are exact
112
+ print(check.efficiency, check.log_evidence) # efficiency near 1 = posterior matched
113
+ ```
114
+
115
+ For an exact sampler on top of a learned prior use `latent_pcn(flow, potential)`: pCN in the flow's
116
+ latent space, whose acceptance needs no Jacobian.
117
+
118
+ ## 3. Amortised posterior (condition on data)
119
+
120
+ ```python
121
+ from FuncFlows.transports.continuous import DataConditioner
122
+ from FuncFlows.objectives import ConditionalFlowMatching
123
+
124
+ C = ... # context dimension (e.g. whitened projected data)
125
+ field = SumField(
126
+ LinearField(M, C, num_time_modes=4, mode_scale=measure.scale), # data drift: the linear-Gaussian mean
127
+ MatrixField(DataConditioner(M, 256, C, num_time_modes=4), mode_scale=measure.scale))
128
+ flow = ContinuousTransformation(measure, field, num_steps=16)
129
+
130
+ def simulate(batch): # -> (target coeffs [batch, M], context [batch, C])
131
+ ...
132
+
133
+ train(ConditionalFlowMatching(flow, simulate, measure.sample, batch_size=256, weights=1 / measure.scale),
134
+ flow.parameters(), num_steps=20000, learning_rate=2e-3)
135
+
136
+ posterior = flow.transport(measure.sample(1000), context.expand(1000, -1)) # one ODE solve per posterior
137
+ ```
138
+
139
+ Whiten the context by `measure.scale` when it is itself a projected function; otherwise the drift
140
+ weights for high modes have to reach 1/σ and never do. When a conjugate (linear-Gaussian)
141
+ posterior is available in closed form, use it as the base measure and let the flow learn the residual.
142
+
143
+ ## Diagnostics
144
+
145
+ ```python
146
+ from FuncFlows.diagnostics import coverage_curve, coverage_error
147
+ levels, coverage = coverage_curve(samples, truths, weights=1 / measure.scale) # TARP; samples [cases, draws, M]
148
+ coverage_error(levels, coverage) # signed max gap from the diagonal: negative = overconfident
149
+ ```
150
+
151
+ ## Layout
152
+
153
+ ```
154
+ FuncFlows/
155
+ base_measures/ bases, GaussianReferenceMeasure
156
+ transports/ continuous/: fields, conditioners, ContinuousTransformation
157
+ (grid_fields.py: PointwiseField / OperatorField, spatial layers — experimental)
158
+ objectives/ flow_matching, reverse_kl, negative_logl
159
+ samplers/ latent_pcn
160
+ diagnostics/ importance, coverage
161
+ utils/ train, gaussian_misfit
162
+ ```
163
+
164
+
165
+
166
+ # Full Example
167
+
168
+
169
+ ```python
170
+ import matplotlib
171
+ matplotlib.use("Agg")
172
+ import matplotlib.pyplot as plt
173
+ import torch
174
+
175
+ from FuncFlows.base_measures import CosineBasis, GaussianReferenceMeasure
176
+ from FuncFlows.transports.continuous import (ContinuousTransformation, SumField, LinearField,
177
+ MatrixField, TimeBasisConditioner)
178
+ from FuncFlows.objectives import ReverseKL, FlowMatching
179
+ from FuncFlows.samplers import latent_pcn
180
+ from FuncFlows.utils.train import train
181
+ from FuncFlows.utils.gaussian_misfit import GaussianMisfit
182
+ from FuncFlows.diagnostics import ImportanceCorrection
183
+
184
+ torch.manual_seed(1)
185
+ M, NUM_OBS, NOISE, N = 32, 15, 0.05, 2000
186
+ basis = CosineBasis(M)
187
+ prior = GaussianReferenceMeasure(basis, alpha=0.05, power=2.0)
188
+ x = torch.linspace(0, 1, 400, dtype=torch.float64)[:, None]
189
+ design = basis.evaluate(x)
190
+
191
+ # data || y = f(x)^2 + noise at NUM_OBS random points
192
+ truth = prior.sample(1)
193
+ obs_x = torch.rand(NUM_OBS, 1, dtype=torch.float64)
194
+ phi_obs = basis.evaluate(obs_x)
195
+ forward = lambda v: (v @ phi_obs.T) ** 2
196
+ data = forward(truth)[0] + NOISE * torch.randn(NUM_OBS, dtype=torch.float64)
197
+ misfit = GaussianMisfit(forward, data, NOISE) # -logl
198
+
199
+
200
+ def make_flow():
201
+ field = SumField(LinearField(M, num_time_modes=4),
202
+ MatrixField(TimeBasisConditioner(M, 256, num_time_modes=4), mode_scale=prior.scale))
203
+ return ContinuousTransformation(prior, field, num_steps=16)
204
+
205
+
206
+ @torch.no_grad()
207
+ def pcn(potential, num_chains=64, num_steps=20000, beta=0.15, burn=5000, thin=20):
208
+ state, keep = prior.sample(num_chains), []
209
+ energy = potential(state)
210
+ for step in range(num_steps):
211
+ proposal = (1 - beta ** 2) ** 0.5 * state + beta * prior.sample(num_chains)
212
+ proposal_energy = potential(proposal)
213
+ accept = torch.log(torch.rand_like(energy)) < energy - proposal_energy
214
+ state = torch.where(accept[:, None], proposal, state)
215
+ energy = torch.where(accept, proposal_energy, energy)
216
+ if step >= burn and step % thin == 0:
217
+ keep.append(state.clone())
218
+ return torch.cat(keep)
219
+
220
+
221
+ def laplace_gp():
222
+ white = torch.zeros(M, dtype=torch.float64, requires_grad=True)
223
+ opt = torch.optim.LBFGS([white], max_iter=200, line_search_fn="strong_wolfe")
224
+
225
+ def closure():
226
+ opt.zero_grad()
227
+ loss = misfit(white * prior.scale) + 0.5 * (white ** 2).sum()
228
+ loss.backward()
229
+ return loss
230
+
231
+ opt.step(closure)
232
+ with torch.no_grad():
233
+ jac = 2 * (phi_obs @ (white * prior.scale))[:, None] * phi_obs * prior.scale
234
+ cov = torch.linalg.inv(torch.eye(M, dtype=torch.float64) + jac.T @ jac / NOISE ** 2)
235
+ return (white + torch.randn(N, M, dtype=torch.float64) @ torch.linalg.cholesky(cov).T) * prior.scale
236
+
237
+
238
+ # run everything
239
+ results = {}
240
+ results["pCN reference"] = pcn(misfit)[torch.randperm(64 * 750)[:N]] # thin to N for the comparisons
241
+
242
+ results["GP (Laplace)"] = laplace_gp()
243
+
244
+ rkl = make_flow()
245
+ train(ReverseKL(rkl, misfit, num_samples=64, path_gradient=True), rkl.parameters(), num_steps=2500, learning_rate=3e-3)
246
+ with torch.no_grad():
247
+ results["reverse KL"] = rkl.transport(prior.sample(N))
248
+
249
+ fm = make_flow()
250
+ train(FlowMatching(fm, results["pCN reference"], batch_size=256), fm.parameters(), num_steps=4000, learning_rate=3e-3)
251
+ with torch.no_grad():
252
+ results["flow matching"] = fm.transport(prior.sample(N))
253
+
254
+ # pCN in the latent space of the posterior flow: target exp(-Phi(Tz)) prior(Tz)/q(Tz) mu0(dz), so add the
255
+ # flow's exact log-density to the potential. A good flow makes the target nearly flat -> big steps accepted.
256
+ corrected = lambda v: misfit(v) + fm.log_rn_at(v) # noqa: E731
257
+ draws, info = latent_pcn(fm, corrected, num_chains=128, num_steps=1500, beta=0.5, thin=10)
258
+ results["latent pCN"] = draws[torch.randperm(len(draws))[:N]]
259
+
260
+
261
+
262
+ # report
263
+ ref = results["pCN reference"] @ design.T
264
+ print(f"\n{'method':16s} {'+sign':>6s} {'E-dist':>8s} (ideal 0.50; distance to pCN)")
265
+ for name, v in results.items():
266
+ values = v @ design.T
267
+ sign = ((v @ truth[0]) > 0).double().mean().item()
268
+ dist = (2 * torch.cdist(values, ref).mean() - torch.cdist(values, values).mean() - torch.cdist(ref, ref).mean()) / 20
269
+ print(f"{name:16s} {sign:6.2f} {dist.item():8.4f}")
270
+ for name, flow in [("reverse KL", rkl), ("flow matching", fm)]:
271
+ with torch.no_grad():
272
+ c = ImportanceCorrection(flow, results[name], misfit)
273
+ print(f"{name}: importance efficiency {c.efficiency:.3f}, log evidence {c.log_evidence:.2f}")
274
+ print(f"latent pCN acceptance {info['acceptance']:.2f} at beta 0.5 (plain pCN would be ~0)")
275
+
276
+
277
+ # figures
278
+ fig, axes = plt.subplots(1, len(results), figsize=(3.6 * len(results), 3.2), sharey=True)
279
+ tv = (truth @ design.T)[0]
280
+ for ax, (name, v) in zip(axes, results.items()):
281
+ ax.plot(x[:, 0], (v[:40] @ design.T).T, color="C0", alpha=0.15, lw=1)
282
+ ax.plot(x[:, 0], tv, "k", lw=1.5)
283
+ ax.plot(x[:, 0], -tv, "k--", lw=1.5)
284
+ ax.scatter(obs_x[:, 0], data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
285
+ ax.scatter(obs_x[:, 0], -data.clamp(min=0).sqrt(), color="C3", s=12, zorder=3)
286
+ ax.set_title(name, fontsize=10)
287
+ fig.suptitle("y = f(x)^2 + noise: black = ±truth, red = ±sqrt(data), blue = posterior draws", fontsize=10)
288
+ fig.tight_layout()
289
+ fig.savefig("bimodal_posterior.png", dpi=130)
290
+ print("saved bimodal_posterior.png")
291
+ ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "funcflows"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "Normalizing flows and flow matching on function spaces, with exact-trace vector fields"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -25,7 +25,7 @@ ot = ["scipy"] # optimal-transport coupling in flow matchi
25
25
  test = ["pytest"]
26
26
 
27
27
  [project.urls]
28
- Homepage = "https://github.com/YOUR_USER/FuncFlows"
28
+ Homepage = "https://pypi.org/project/funcflows/"
29
29
 
30
30
  [tool.setuptools.packages.find]
31
31
  include = ["FuncFlows*"]
funcflows-0.1.2/PKG-INFO DELETED
@@ -1,43 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: funcflows
3
- Version: 0.1.2
4
- Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
5
- Author: Liam Pinchbeck
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/YOUR_USER/FuncFlows
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Intended Audience :: Science/Research
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Topic :: Scientific/Engineering
13
- Requires-Python: >=3.10
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: torch
17
- Requires-Dist: torchvision
18
- Requires-Dist: tqdm
19
- Requires-Dist: scipy
20
- Requires-Dist: matplotlib
21
- Provides-Extra: ot
22
- Requires-Dist: scipy; extra == "ot"
23
- Provides-Extra: test
24
- Requires-Dist: pytest; extra == "test"
25
- Dynamic: license-file
26
-
27
- # FuncFlows
28
-
29
- Normalizing flows and flow matching on function spaces. Functions are represented by
30
- coefficients on a Laplacian eigenbasis (cosine or Fourier), the reference measure is a Gaussian
31
- on those coefficients, and transports are neural ODEs whose vector fields (`LinearField`,
32
- `MatrixField`, `PointwiseField`) all have closed-form divergences, so densities relative to the
33
- reference measure are exact.
34
-
35
- ```bash
36
- pip install funcflows
37
- ```
38
-
39
- Objectives: flow matching (conditional / amortised), reverse KL, alpha-divergences, negative
40
- log-likelihood. Samplers and diagnostics: latent-space pCN, importance correction, TARP coverage.
41
-
42
- `torch` is a hard dependency and is large; install it first from pytorch.org if you need a
43
- specific CUDA/MPS build.
funcflows-0.1.2/README.md DELETED
@@ -1,17 +0,0 @@
1
- # FuncFlows
2
-
3
- Normalizing flows and flow matching on function spaces. Functions are represented by
4
- coefficients on a Laplacian eigenbasis (cosine or Fourier), the reference measure is a Gaussian
5
- on those coefficients, and transports are neural ODEs whose vector fields (`LinearField`,
6
- `MatrixField`, `PointwiseField`) all have closed-form divergences, so densities relative to the
7
- reference measure are exact.
8
-
9
- ```bash
10
- pip install funcflows
11
- ```
12
-
13
- Objectives: flow matching (conditional / amortised), reverse KL, alpha-divergences, negative
14
- log-likelihood. Samplers and diagnostics: latent-space pCN, importance correction, TARP coverage.
15
-
16
- `torch` is a hard dependency and is large; install it first from pytorch.org if you need a
17
- specific CUDA/MPS build.
@@ -1,43 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: funcflows
3
- Version: 0.1.2
4
- Summary: Normalizing flows and flow matching on function spaces, with exact-trace vector fields
5
- Author: Liam Pinchbeck
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/YOUR_USER/FuncFlows
8
- Classifier: Development Status :: 3 - Alpha
9
- Classifier: Intended Audience :: Science/Research
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Topic :: Scientific/Engineering
13
- Requires-Python: >=3.10
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: torch
17
- Requires-Dist: torchvision
18
- Requires-Dist: tqdm
19
- Requires-Dist: scipy
20
- Requires-Dist: matplotlib
21
- Provides-Extra: ot
22
- Requires-Dist: scipy; extra == "ot"
23
- Provides-Extra: test
24
- Requires-Dist: pytest; extra == "test"
25
- Dynamic: license-file
26
-
27
- # FuncFlows
28
-
29
- Normalizing flows and flow matching on function spaces. Functions are represented by
30
- coefficients on a Laplacian eigenbasis (cosine or Fourier), the reference measure is a Gaussian
31
- on those coefficients, and transports are neural ODEs whose vector fields (`LinearField`,
32
- `MatrixField`, `PointwiseField`) all have closed-form divergences, so densities relative to the
33
- reference measure are exact.
34
-
35
- ```bash
36
- pip install funcflows
37
- ```
38
-
39
- Objectives: flow matching (conditional / amortised), reverse KL, alpha-divergences, negative
40
- log-likelihood. Samplers and diagnostics: latent-space pCN, importance correction, TARP coverage.
41
-
42
- `torch` is a hard dependency and is large; install it first from pytorch.org if you need a
43
- specific CUDA/MPS build.
File without changes
File without changes
File without changes