funcflows 0.1.2__py3-none-any.whl
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.
- FuncFlows/__init__.py +0 -0
- FuncFlows/base_measures/__init__.py +3 -0
- FuncFlows/base_measures/abstract_reference_measure.py +28 -0
- FuncFlows/base_measures/bases.py +112 -0
- FuncFlows/base_measures/gaussian_reference_measure.py +37 -0
- FuncFlows/diagnostics/__init__.py +2 -0
- FuncFlows/diagnostics/coverage.py +46 -0
- FuncFlows/diagnostics/importance.py +60 -0
- FuncFlows/objectives/__init__.py +5 -0
- FuncFlows/objectives/alpha_divergence.py +67 -0
- FuncFlows/objectives/flow_matching.py +132 -0
- FuncFlows/objectives/negative_logl.py +15 -0
- FuncFlows/objectives/reverse_kl.py +48 -0
- FuncFlows/samplers/__init__.py +1 -0
- FuncFlows/samplers/latent_pcn.py +53 -0
- FuncFlows/transports/__init__.py +3 -0
- FuncFlows/transports/abstract_transformation.py +32 -0
- FuncFlows/transports/continuous/__init__.py +6 -0
- FuncFlows/transports/continuous/base_continuous.py +63 -0
- FuncFlows/transports/continuous/conditioners.py +83 -0
- FuncFlows/transports/continuous/grid_fields.py +403 -0
- FuncFlows/transports/continuous/vector_fields.py +306 -0
- FuncFlows/transports/layers/__init__.py +2 -0
- FuncFlows/transports/layers/base_discrete.py +29 -0
- FuncFlows/transports/layers/layer_classes.py +81 -0
- FuncFlows/utils/__init__.py +0 -0
- FuncFlows/utils/gaussian_misfit.py +16 -0
- FuncFlows/utils/train.py +31 -0
- funcflows-0.1.2.dist-info/METADATA +43 -0
- funcflows-0.1.2.dist-info/RECORD +33 -0
- funcflows-0.1.2.dist-info/WHEEL +5 -0
- funcflows-0.1.2.dist-info/licenses/LICENSE +21 -0
- funcflows-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from FuncFlows.base_measures import ReferenceMeasure
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Transformation(torch.nn.Module):
|
|
7
|
+
"""A measurable bijection of coefficient space, with its Radon-Nikodym derivative.
|
|
8
|
+
|
|
9
|
+
`context` threads through every method so conditional fields work end to end; unconditional
|
|
10
|
+
transformations simply ignore it.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, base_measure: ReferenceMeasure):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.base_measure = base_measure
|
|
16
|
+
|
|
17
|
+
def _map(self, coeffs, context=None):
|
|
18
|
+
"""coeffs [..., num_functions] -> (coeffs_out [..., num_functions], log_det_term [...])
|
|
19
|
+
log_det_term = log|det Df| for a discrete map, integral of Tr Dh for an ODE."""
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
|
|
22
|
+
def pull_back(self, coeffs_out, context=None):
|
|
23
|
+
"""Inverse map. Optional; needed only by NLL / learned-prior uses."""
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
26
|
+
def push_forward(self, coeffs, context=None):
|
|
27
|
+
"""-> (coeffs_out, log_rn_weight) with log_rn_weight = log d(f#mu)/d(mu) at coeffs_out."""
|
|
28
|
+
coeffs_out, log_det_term = self._map(coeffs, context)
|
|
29
|
+
return coeffs_out, self.base_measure.log_density_diff(coeffs, coeffs_out) - log_det_term
|
|
30
|
+
|
|
31
|
+
def log_rn_at(self, coeffs_out, context=None):
|
|
32
|
+
return self.push_forward(self.pull_back(coeffs_out, context), context)[1]
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
from .vector_fields import (LinearField, MatrixField,
|
|
2
|
+
SumField, VectorField, TimeCosines)
|
|
3
|
+
from .base_continuous import ContinuousTransformation
|
|
4
|
+
from .conditioners import (Conditioner, TimeBasisConditioner,
|
|
5
|
+
DataConditioner)
|
|
6
|
+
from.grid_fields import (PointwiseField, OperatorField, GridTransform)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from ..abstract_transformation import Transformation
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContinuousTransformation(Transformation):
|
|
7
|
+
"""f = the time-T flow of dv/dt = h(v,t). log_det_term is the integral of Tr Dh (P2 Thm 5).
|
|
8
|
+
|
|
9
|
+
One integrator serves every use. with_trace=False skips the log-determinant entirely, which
|
|
10
|
+
is what transport (sampling) and pull_back need; with_trace=True uses velocity_and_trace so
|
|
11
|
+
the trace shares the field evaluation instead of repeating it. The old version recomputed the
|
|
12
|
+
field twice per RK4 stage in every density-bearing solve.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, base_measure, vector_field, total_time=1.0, num_steps=20, method="rk4"):
|
|
16
|
+
super().__init__(base_measure)
|
|
17
|
+
self.vector_field, self.total_time, self.num_steps, self.method = vector_field, total_time, num_steps, method
|
|
18
|
+
|
|
19
|
+
def _stage(self, coeffs, time_value, context, with_trace):
|
|
20
|
+
if with_trace:
|
|
21
|
+
return self.vector_field.velocity_and_trace(coeffs, time_value, context)
|
|
22
|
+
return self.vector_field.velocity(coeffs, time_value, context), None
|
|
23
|
+
|
|
24
|
+
def _integrate(self, coeffs, forward=True, context=None, with_trace=True):
|
|
25
|
+
step = (self.total_time / self.num_steps) * (1 if forward else -1)
|
|
26
|
+
time_value = 0.0 if forward else self.total_time
|
|
27
|
+
trace_total = torch.zeros_like(coeffs[..., 0]) if with_trace else None
|
|
28
|
+
for _ in range(self.num_steps):
|
|
29
|
+
velocity_1, trace_1 = self._stage(coeffs, time_value, context, with_trace)
|
|
30
|
+
if self.method == "euler":
|
|
31
|
+
velocity, trace = velocity_1, trace_1
|
|
32
|
+
else:
|
|
33
|
+
velocity_2, trace_2 = self._stage(coeffs + 0.5 * step * velocity_1,
|
|
34
|
+
time_value + 0.5 * step, context, with_trace)
|
|
35
|
+
velocity_3, trace_3 = self._stage(coeffs + 0.5 * step * velocity_2,
|
|
36
|
+
time_value + 0.5 * step, context, with_trace)
|
|
37
|
+
velocity_4, trace_4 = self._stage(coeffs + step * velocity_3,
|
|
38
|
+
time_value + step, context, with_trace)
|
|
39
|
+
velocity = (velocity_1 + 2 * velocity_2 + 2 * velocity_3 + velocity_4) / 6
|
|
40
|
+
trace = ((trace_1 + 2 * trace_2 + 2 * trace_3 + trace_4) / 6) if with_trace else None
|
|
41
|
+
coeffs = coeffs + step * velocity
|
|
42
|
+
if with_trace:
|
|
43
|
+
trace_total = trace_total + step * trace
|
|
44
|
+
time_value = time_value + step
|
|
45
|
+
return coeffs, trace_total
|
|
46
|
+
|
|
47
|
+
def _map(self, coeffs, context=None):
|
|
48
|
+
return self._integrate(coeffs, forward=True, context=context)
|
|
49
|
+
|
|
50
|
+
def pull_back(self, coeffs_out, context=None):
|
|
51
|
+
return self._integrate(coeffs_out, forward=False, context=context, with_trace=False)[0]
|
|
52
|
+
|
|
53
|
+
def log_rn_at(self, coeffs_out, context=None):
|
|
54
|
+
"""One backward solve instead of pull_back + push_forward (P2 Supp Thm 2.1)."""
|
|
55
|
+
coeffs, trace_total = self._integrate(coeffs_out, forward=False, context=context)
|
|
56
|
+
return self.base_measure.log_density_diff(coeffs, coeffs_out) + trace_total
|
|
57
|
+
|
|
58
|
+
def transport(self, coeffs, context=None):
|
|
59
|
+
"""Integrate the field only. No trace, no log-determinant — for sampling."""
|
|
60
|
+
return self._integrate(coeffs, forward=True, context=context, with_trace=False)[0]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
|
|
3
|
+
from .vector_fields import TimeCosines
|
|
4
|
+
|
|
5
|
+
# base class
|
|
6
|
+
class Conditioner(torch.nn.Module):
|
|
7
|
+
"""(time, context) -> (output_matrix [num_functions, num_terms],
|
|
8
|
+
input_matrix [num_terms, num_functions],
|
|
9
|
+
biases [num_terms] or [batch, num_terms])"""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def __init__(self, num_functions, num_terms):
|
|
13
|
+
super().__init__()
|
|
14
|
+
self.num_functions, self.num_terms = num_functions, num_terms
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def forward(self, time_value, context=None):
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Just a class for conditioners in the velocity fields script
|
|
23
|
+
# that handles unknown parameters that are functions of just time
|
|
24
|
+
class TimeBasisConditioner(Conditioner):
|
|
25
|
+
"""A(t) = sum_m cos(m pi t / T) A_m, same for B and the biases. Genuine time dependence
|
|
26
|
+
for num_time_modes * 2 * L * r parameters, instead of a net emitting that many per step.
|
|
27
|
+
|
|
28
|
+
The contraction over m is a tensordot, which never materialises the [T, r, L] product the
|
|
29
|
+
broadcast-and-sum form did — that intermediate was rebuilt four times per RK4 step.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, num_functions, num_terms, num_time_modes=4, total_time=1.0,
|
|
33
|
+
init_scale=0.05, dtype=torch.float64):
|
|
34
|
+
super().__init__(num_functions, num_terms)
|
|
35
|
+
scale = init_scale / (num_functions * num_time_modes) ** 0.5
|
|
36
|
+
self.total_time, self.num_time_modes = total_time, num_time_modes
|
|
37
|
+
|
|
38
|
+
self.output_stack = torch.nn.Parameter(scale * torch.randn(num_time_modes, num_functions, num_terms, dtype=dtype))
|
|
39
|
+
self.input_stack = torch.nn.Parameter(scale * torch.randn(num_time_modes, num_terms, num_functions, dtype=dtype))
|
|
40
|
+
self.bias_stack = torch.nn.Parameter(torch.zeros(num_time_modes, num_terms, dtype=dtype))
|
|
41
|
+
|
|
42
|
+
self.cosines = TimeCosines(num_time_modes, total_time, dtype) # cached per scalar time
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def time_weights(self, time_value):
|
|
46
|
+
return self.cosines(time_value)
|
|
47
|
+
|
|
48
|
+
def forward(self, time_value, context=None):
|
|
49
|
+
weights = self.time_weights(time_value)
|
|
50
|
+
return (torch.tensordot(weights, self.output_stack, dims=1),
|
|
51
|
+
torch.tensordot(weights, self.input_stack, dims=1),
|
|
52
|
+
weights @ self.bias_stack)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Just a class for conditioners in the velocity fields script
|
|
57
|
+
# that handles unknown parameters that are functions of time AND data embeddings
|
|
58
|
+
class DataConditioner(TimeBasisConditioner):
|
|
59
|
+
"""Same field, but the data shifts the bias: b(t, c) = sum_m w_m(t) [bias_m + c @ context_m.T].
|
|
60
|
+
|
|
61
|
+
The context does not depend on v, so dh/dv is unchanged and MatrixField.trace still holds.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, num_functions, num_terms, context_dim, num_time_modes=4,
|
|
65
|
+
total_time=1.0, init_scale=0.05, dtype=torch.float64):
|
|
66
|
+
super().__init__(num_functions, num_terms, num_time_modes, total_time, init_scale, dtype)
|
|
67
|
+
|
|
68
|
+
self.context_stack = torch.nn.Parameter(
|
|
69
|
+
init_scale / (context_dim * num_time_modes) ** 0.5
|
|
70
|
+
* torch.randn(num_time_modes, num_terms, context_dim, dtype=dtype))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def forward(self, time_value, context=None):
|
|
75
|
+
output_matrix, input_matrix, biases = super().forward(time_value)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if context is not None:
|
|
79
|
+
weights = self.time_weights(time_value)
|
|
80
|
+
biases = biases + context @ torch.tensordot(weights, self.context_stack, dims=1).T
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
return output_matrix, input_matrix, biases
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
|
|
2
|
+
import torch
|
|
3
|
+
from .vector_fields import VectorField, TimeCosines, combine_context
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
################################################################################################################
|
|
7
|
+
################################################################################################################
|
|
8
|
+
################################################################################################################
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GridTransform(torch.nn.Module):
|
|
12
|
+
"""Coefficients <-> values on a uniform grid, and back, for one fixed basis truncation."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, basis, num_active, grid_size, mode="auto", dtype=torch.float64):
|
|
15
|
+
super().__init__()
|
|
16
|
+
|
|
17
|
+
self.num_active, self.grid_size, self.dtype = num_active, grid_size, dtype
|
|
18
|
+
self.physical_dim = basis.physical_dim
|
|
19
|
+
self.quadrature_weight = grid_size ** (-basis.physical_dim)
|
|
20
|
+
device = basis.laplacian_eigenvalues.device
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# cell-edge grid, so the FFT needs no midpoint phase; on a periodic uniform grid the
|
|
24
|
+
# rectangle rule is spectrally accurate either way
|
|
25
|
+
axis = torch.arange(grid_size, dtype=dtype, device=device) / grid_size
|
|
26
|
+
points = torch.cartesian_prod(*[axis] * basis.physical_dim).reshape(-1, basis.physical_dim)
|
|
27
|
+
values = basis.evaluate(points)[:, :num_active]
|
|
28
|
+
|
|
29
|
+
self.register_buffer("basis_grid", values) # [grid^d, num_active]
|
|
30
|
+
|
|
31
|
+
self.use_fft = False
|
|
32
|
+
|
|
33
|
+
if mode != "dense" and hasattr(basis, "wavenumbers"): # separable Fourier basis only
|
|
34
|
+
self._build_tables(basis, num_active, grid_size, device)
|
|
35
|
+
self.use_fft = self._agrees(values)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if mode == "fft" and not self.use_fft:
|
|
39
|
+
raise RuntimeError("the FFT transform needs a FourierBasis and must pass its check; "
|
|
40
|
+
"pass transform='dense'")
|
|
41
|
+
|
|
42
|
+
self.register_buffer("basis_values", None if self.use_fft else values)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _build_tables(self, basis, num_active, grid_size, device):
|
|
47
|
+
"""Per-axis mode index m: 0 -> constant, odd -> cos with k=(m+1)//2, even>0 -> sin with k=m//2."""
|
|
48
|
+
wavenumbers = basis.wavenumbers[:num_active].round().long() # [num_active, d]
|
|
49
|
+
|
|
50
|
+
is_sine = basis.phases[:num_active] != 0
|
|
51
|
+
|
|
52
|
+
modes = torch.where(wavenumbers == 0, torch.zeros_like(wavenumbers),
|
|
53
|
+
2 * wavenumbers - 1 + is_sine.long())
|
|
54
|
+
|
|
55
|
+
self.max_axis_mode = int(modes.max().item()) + 1
|
|
56
|
+
highest = int(wavenumbers.max().item())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
if highest >= grid_size // 2:
|
|
60
|
+
raise ValueError(f"grid_size {grid_size} is below Nyquist for wavenumber {highest}; "
|
|
61
|
+
f"use at least {2 * highest + 2}")
|
|
62
|
+
flat = torch.zeros(num_active, dtype=torch.long, device=device)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
for axis in range(self.physical_dim):
|
|
66
|
+
flat = flat * self.max_axis_mode + modes[:, axis]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
self.register_buffer("flat_index", flat)
|
|
71
|
+
axis_modes = torch.arange(1, self.max_axis_mode, device=device)
|
|
72
|
+
cosine, sine = axis_modes[axis_modes % 2 == 1], axis_modes[axis_modes % 2 == 0]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
self.register_buffer("cosine_mode", cosine)
|
|
76
|
+
self.register_buffer("cosine_wave", (cosine + 1) // 2)
|
|
77
|
+
self.register_buffer("sine_mode", sine)
|
|
78
|
+
self.register_buffer("sine_wave", sine // 2)
|
|
79
|
+
self.register_buffer("zero_index", torch.zeros(1, dtype=torch.long, device=device))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _synthesise_axis(self, coefficients):
|
|
84
|
+
"""[..., max_axis_mode] -> [..., grid_size] along the last axis."""
|
|
85
|
+
shape = coefficients.shape[:-1] + (self.grid_size // 2 + 1,)
|
|
86
|
+
scale = self.grid_size / 2 ** 0.5
|
|
87
|
+
|
|
88
|
+
real = torch.zeros(shape, dtype=coefficients.dtype, device=coefficients.device)
|
|
89
|
+
real = real.index_add(-1, self.zero_index, self.grid_size * coefficients[..., :1])
|
|
90
|
+
real = real.index_add(-1, self.cosine_wave, scale * coefficients[..., self.cosine_mode])
|
|
91
|
+
|
|
92
|
+
imaginary = torch.zeros(shape, dtype=coefficients.dtype, device=coefficients.device)
|
|
93
|
+
imaginary = imaginary.index_add(-1, self.sine_wave, -scale * coefficients[..., self.sine_mode])
|
|
94
|
+
|
|
95
|
+
return torch.fft.irfft(torch.complex(real, imaginary), n=self.grid_size, dim=-1)
|
|
96
|
+
|
|
97
|
+
def _analyse_axis(self, values):
|
|
98
|
+
spectrum = torch.fft.rfft(values, dim=-1)
|
|
99
|
+
scale = 2 ** 0.5 / self.grid_size
|
|
100
|
+
|
|
101
|
+
coefficients = torch.zeros(values.shape[:-1] + (self.max_axis_mode,),
|
|
102
|
+
dtype=values.dtype, device=values.device)
|
|
103
|
+
|
|
104
|
+
coefficients = coefficients.index_add(-1, self.zero_index, spectrum.real[..., :1] / self.grid_size)
|
|
105
|
+
coefficients = coefficients.index_add(-1, self.cosine_mode, scale * spectrum.real[..., self.cosine_wave])
|
|
106
|
+
|
|
107
|
+
return coefficients.index_add(-1, self.sine_mode, -scale * spectrum.imag[..., self.sine_wave])
|
|
108
|
+
|
|
109
|
+
def to_grid(self, coeffs):
|
|
110
|
+
"""[..., num_active] -> [..., grid_size^d]."""
|
|
111
|
+
if not self.use_fft:
|
|
112
|
+
return coeffs @ self.basis_values.T
|
|
113
|
+
|
|
114
|
+
block = torch.zeros(coeffs.shape[:-1] + (self.max_axis_mode ** self.physical_dim,),
|
|
115
|
+
dtype=coeffs.dtype, device=coeffs.device)
|
|
116
|
+
block = block.index_add(-1, self.flat_index, coeffs)
|
|
117
|
+
block = block.reshape(coeffs.shape[:-1] + (self.max_axis_mode,) * self.physical_dim)
|
|
118
|
+
|
|
119
|
+
for _ in range(self.physical_dim):
|
|
120
|
+
block = self._synthesise_axis(block).movedim(-1, -self.physical_dim)
|
|
121
|
+
|
|
122
|
+
return block.reshape(coeffs.shape[:-1] + (self.grid_size ** self.physical_dim,))
|
|
123
|
+
|
|
124
|
+
def from_grid(self, values):
|
|
125
|
+
"""[..., grid_size^d] -> [..., num_active] projection."""
|
|
126
|
+
if not self.use_fft:
|
|
127
|
+
return (values @ self.basis_values) * self.quadrature_weight
|
|
128
|
+
|
|
129
|
+
block = values.reshape(values.shape[:-1] + (self.grid_size,) * self.physical_dim)
|
|
130
|
+
|
|
131
|
+
for _ in range(self.physical_dim):
|
|
132
|
+
block = self._analyse_axis(block).movedim(-1, -self.physical_dim)
|
|
133
|
+
|
|
134
|
+
return block.reshape(values.shape[:-1]
|
|
135
|
+
+ (self.max_axis_mode ** self.physical_dim,))[..., self.flat_index]
|
|
136
|
+
|
|
137
|
+
def _agrees(self, values, tolerance=None):
|
|
138
|
+
"""The FFT path is a fixed reindexing of the dense one, so it should agree to round-off."""
|
|
139
|
+
tolerance = tolerance or (1e-4 if self.dtype is torch.float32 else 1e-9)
|
|
140
|
+
|
|
141
|
+
probe = torch.randn(3, self.num_active, dtype=self.dtype, device=values.device)
|
|
142
|
+
grid_probe = torch.randn(3, values.shape[0], dtype=self.dtype, device=values.device)
|
|
143
|
+
self.use_fft = True
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
forward = (self.to_grid(probe) - probe @ values.T).abs().max().item()
|
|
147
|
+
backward = (self.from_grid(grid_probe)
|
|
148
|
+
- (grid_probe @ values) * self.quadrature_weight).abs().max().item()
|
|
149
|
+
except Exception:
|
|
150
|
+
self.use_fft = False
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
self.use_fft = False
|
|
154
|
+
scale = max(1.0, probe.abs().max().item())
|
|
155
|
+
return bool(forward < tolerance * scale and backward < tolerance * scale)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
################################################################################################################
|
|
163
|
+
################################################################################################################
|
|
164
|
+
################################################################################################################
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class PointwiseField(VectorField):
|
|
168
|
+
"""h(v,t)(x) = a(x) tanh( g(x) v(x) + (K_t v)(x) + c(x) ) — one Fourier-neural-operator layer.
|
|
169
|
+
|
|
170
|
+
Taken from arXiv:2010.08895
|
|
171
|
+
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
supports_batched_time = True
|
|
175
|
+
|
|
176
|
+
def __init__(self, basis, num_active, grid_size, num_time_modes=4, num_field_modes=32,
|
|
177
|
+
num_spectral_modes=0, context_dim=0, total_time=1.0, mode_scale=None,
|
|
178
|
+
init_scale=0.05, transform="auto", time_pair=False, dtype=torch.float64):
|
|
179
|
+
super().__init__()
|
|
180
|
+
self.num_active, self.num_time_modes = num_active, num_time_modes
|
|
181
|
+
self.total_time, self.mode_scale, self.dtype = total_time, mode_scale, dtype
|
|
182
|
+
self.physical_dim, self.grid_size = basis.physical_dim, grid_size
|
|
183
|
+
self.quadrature_weight = grid_size ** (-basis.physical_dim)
|
|
184
|
+
|
|
185
|
+
self.cosines = TimeCosines(num_time_modes, total_time, dtype, time_pair)
|
|
186
|
+
|
|
187
|
+
self.transform = GridTransform(basis, num_active, grid_size, mode=transform, dtype=dtype)
|
|
188
|
+
|
|
189
|
+
self.quadrature_weight = self.transform.quadrature_weight
|
|
190
|
+
device = basis.laplacian_eigenvalues.device
|
|
191
|
+
values = self.transform.basis_grid
|
|
192
|
+
|
|
193
|
+
self.register_buffer("squared_sum", (values ** 2).sum(1)) # S(x), [grid^d]
|
|
194
|
+
self.register_buffer("field_values", values[:, :num_field_modes]) # a, g, c stay dense
|
|
195
|
+
|
|
196
|
+
gain_scale = init_scale / num_field_modes ** 0.5
|
|
197
|
+
self.gain_stack = torch.nn.Parameter(
|
|
198
|
+
gain_scale * torch.randn(num_time_modes, num_field_modes, dtype=dtype))
|
|
199
|
+
|
|
200
|
+
self.slope_stack = torch.nn.Parameter(torch.zeros(num_time_modes, num_field_modes, dtype=dtype))
|
|
201
|
+
self.shift_stack = torch.nn.Parameter(torch.zeros(num_time_modes, num_field_modes, dtype=dtype))
|
|
202
|
+
|
|
203
|
+
with torch.no_grad(): # phi_0 = 1, so this makes g(x) a constant.
|
|
204
|
+
# whitened coefficients are O(1) each, so the whitened field is O(sqrt(num_active))
|
|
205
|
+
self.slope_stack[0, 0] = 1.0 if mode_scale is None else num_active ** -0.5
|
|
206
|
+
|
|
207
|
+
self.context_stack = None
|
|
208
|
+
if context_dim:
|
|
209
|
+
self.context_stack = torch.nn.Parameter(
|
|
210
|
+
init_scale / context_dim ** 0.5
|
|
211
|
+
* torch.randn(num_time_modes, num_field_modes, context_dim, dtype=dtype))
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
self.kappa_stack = None
|
|
215
|
+
if num_spectral_modes:
|
|
216
|
+
logs = torch.log1p(basis.laplacian_eigenvalues[:num_active].to(dtype))
|
|
217
|
+
span = (logs.max() - logs.min()).clamp(min=1e-12)
|
|
218
|
+
position = ((logs - logs.min()) / span).clamp(0, 1)
|
|
219
|
+
orders = torch.arange(num_spectral_modes, dtype=dtype, device=device)
|
|
220
|
+
self.register_buffer("spectral_features", torch.cos(torch.pi * orders * position[:, None]))
|
|
221
|
+
self.register_buffer("squared_basis", values ** 2) # [grid^d, num_active]
|
|
222
|
+
# zero, not init_scale * randn: the slope path is normalised by num_active^-1/2 so the
|
|
223
|
+
# pre-activation is O(1), but kappa multiplies whitened coefficients that sum over all
|
|
224
|
+
# num_active modes. At 0.05 * randn that term is O(0.17 * sqrt(2000)) ~ 7.6, tanh is
|
|
225
|
+
# flat, (1 - hidden^2) ~ 0, and neither the trace nor any gradient gets through.
|
|
226
|
+
self.kappa_stack = torch.nn.Parameter(
|
|
227
|
+
torch.zeros(num_time_modes, num_spectral_modes, dtype=dtype))
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
################################################################################
|
|
231
|
+
################################################################################
|
|
232
|
+
# field
|
|
233
|
+
|
|
234
|
+
def _profiles(self, time_value, context):
|
|
235
|
+
weights = self.cosines(time_value)
|
|
236
|
+
gain = (weights @ self.gain_stack) @ self.field_values.T
|
|
237
|
+
slope = (weights @ self.slope_stack) @ self.field_values.T
|
|
238
|
+
shift_coeffs = weights @ self.shift_stack
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
if context is not None and self.context_stack is not None:
|
|
242
|
+
shift_coeffs = shift_coeffs + combine_context(self.context_stack, context, weights)
|
|
243
|
+
|
|
244
|
+
multiplier = None
|
|
245
|
+
if self.kappa_stack is not None:
|
|
246
|
+
multiplier = (weights @ self.kappa_stack) @ self.spectral_features.T
|
|
247
|
+
|
|
248
|
+
return gain, slope, shift_coeffs @ self.field_values.T, multiplier
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _hidden(self, coeffs, time_value, context):
|
|
253
|
+
gain, slope, shift, multiplier = self._profiles(time_value, context)
|
|
254
|
+
active = coeffs[..., :self.num_active]
|
|
255
|
+
|
|
256
|
+
if self.mode_scale is not None:
|
|
257
|
+
active = active / self.mode_scale[:self.num_active]
|
|
258
|
+
|
|
259
|
+
if multiplier is None:
|
|
260
|
+
pre_activation = slope * self.transform.to_grid(active) + shift
|
|
261
|
+
else: # one transform for both fields, not two
|
|
262
|
+
both = self.transform.to_grid(torch.stack([active, active * multiplier]))
|
|
263
|
+
pre_activation = slope * both[0] + both[1] + shift # + (K v)(x)
|
|
264
|
+
|
|
265
|
+
return gain, slope, multiplier, torch.tanh(pre_activation)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _update(self, coeffs, gain, hidden):
|
|
270
|
+
update = self.transform.from_grid(gain * hidden)
|
|
271
|
+
|
|
272
|
+
if self.mode_scale is not None:
|
|
273
|
+
update = update * self.mode_scale[:self.num_active]
|
|
274
|
+
|
|
275
|
+
return torch.nn.functional.pad(update, (0, coeffs.shape[-1] - self.num_active))
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _trace(self, gain, slope, multiplier, hidden):
|
|
280
|
+
local = slope * self.squared_sum
|
|
281
|
+
if multiplier is not None:
|
|
282
|
+
local = local + multiplier @ self.squared_basis.T # T(x) = sum_j kappa_j phi_j^2
|
|
283
|
+
return ((gain * local) * (1 - hidden ** 2)).sum(-1) * self.quadrature_weight
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def use_fft(self):
|
|
289
|
+
"""Lives on the transform now. Kept here so callers written before it was factored out
|
|
290
|
+
(the scripts' preflight, among others) don't explode."""
|
|
291
|
+
return self.transform.use_fft
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def velocity(self, coeffs, time_value, context=None):
|
|
296
|
+
|
|
297
|
+
gain, _, _, hidden = self._hidden(coeffs, time_value, context)
|
|
298
|
+
|
|
299
|
+
return self._update(coeffs, gain, hidden)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def trace(self, coeffs, time_value, context=None):
|
|
304
|
+
gain, slope, multiplier, hidden = self._hidden(coeffs, time_value, context)
|
|
305
|
+
|
|
306
|
+
return self._trace(gain, slope, multiplier, hidden)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def velocity_and_trace(self, coeffs, time_value, context=None):
|
|
311
|
+
|
|
312
|
+
gain, slope, multiplier, hidden = self._hidden(coeffs, time_value, context)
|
|
313
|
+
|
|
314
|
+
return self._update(coeffs, gain, hidden), self._trace(gain, slope, multiplier, hidden)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
################################################################################################################
|
|
325
|
+
################################################################################################################
|
|
326
|
+
################################################################################################################
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class OperatorField(VectorField):
|
|
330
|
+
"""A stack of Fourier-neural-operator layers as the velocity (FOT-CFM backbone).
|
|
331
|
+
|
|
332
|
+
Took from arXiv:1810.01367
|
|
333
|
+
|
|
334
|
+
z_0 = lift * v(x)
|
|
335
|
+
z_(l+1) = tanh( W_l z_l + K_l z_l + b_l ) W_l pointwise, K_l mode-diagonal
|
|
336
|
+
h = project . z_L, projected back onto the basis
|
|
337
|
+
"""
|
|
338
|
+
|
|
339
|
+
supports_batched_time = True
|
|
340
|
+
|
|
341
|
+
def __init__(self, basis, num_active, grid_size, num_channels=4, num_layers=3,
|
|
342
|
+
num_time_modes=4, num_spectral_modes=8, context_dim=0, total_time=1.0,
|
|
343
|
+
mode_scale=None, init_scale=0.5, transform="auto", time_pair=False,
|
|
344
|
+
trace_samples=1, dtype=torch.float64):
|
|
345
|
+
super().__init__()
|
|
346
|
+
self.num_active, self.num_layers, self.num_channels = num_active, num_layers, num_channels
|
|
347
|
+
self.total_time, self.mode_scale, self.dtype = total_time, mode_scale, dtype
|
|
348
|
+
self.trace_samples = trace_samples
|
|
349
|
+
self.cosines = TimeCosines(num_time_modes, total_time, dtype, time_pair)
|
|
350
|
+
self.transform = GridTransform(basis, num_active, grid_size, mode=transform, dtype=dtype)
|
|
351
|
+
|
|
352
|
+
logs = torch.log1p(basis.laplacian_eigenvalues[:num_active].to(dtype))
|
|
353
|
+
span = (logs.max() - logs.min()).clamp(min=1e-12)
|
|
354
|
+
position = ((logs - logs.min()) / span).clamp(0, 1)
|
|
355
|
+
orders = torch.arange(num_spectral_modes, dtype=dtype, device=logs.device)
|
|
356
|
+
# smooth in log wavenumber, so the multipliers do not depend on where the basis is cut
|
|
357
|
+
self.register_buffer("spectral_features", torch.cos(torch.pi * orders * position[:, None]))
|
|
358
|
+
# a whitened v(x) is O(sqrt(num_active)), so the lift has to undo that or every tanh
|
|
359
|
+
# starts saturated and nothing propagates (the same trap as the kappa init)
|
|
360
|
+
self.register_buffer("lift", torch.full((num_channels,),
|
|
361
|
+
1.0 if mode_scale is None else num_active ** -0.5,
|
|
362
|
+
dtype=dtype))
|
|
363
|
+
shape = (num_layers, num_time_modes, num_channels, num_channels)
|
|
364
|
+
self.pointwise_stack = torch.nn.Parameter(
|
|
365
|
+
init_scale / (num_channels * num_time_modes) ** 0.5 * torch.randn(shape, dtype=dtype))
|
|
366
|
+
self.spectral_stack = torch.nn.Parameter(torch.zeros(shape + (num_spectral_modes,), dtype=dtype))
|
|
367
|
+
self.bias_stack = torch.nn.Parameter(
|
|
368
|
+
torch.zeros(num_layers, num_time_modes, num_channels, dtype=dtype))
|
|
369
|
+
self.project_stack = torch.nn.Parameter(
|
|
370
|
+
init_scale / (num_channels * num_time_modes) ** 0.5
|
|
371
|
+
* torch.randn(num_time_modes, num_channels, dtype=dtype))
|
|
372
|
+
self.context_stack = None
|
|
373
|
+
if context_dim:
|
|
374
|
+
self.context_stack = torch.nn.Parameter(
|
|
375
|
+
init_scale / (context_dim * num_time_modes) ** 0.5
|
|
376
|
+
* torch.randn(num_layers, num_time_modes, num_channels, context_dim, dtype=dtype))
|
|
377
|
+
|
|
378
|
+
def velocity(self, coeffs, time_value, context=None):
|
|
379
|
+
weights = self.cosines(time_value)
|
|
380
|
+
active = coeffs[..., :self.num_active]
|
|
381
|
+
if self.mode_scale is not None:
|
|
382
|
+
active = active / self.mode_scale[:self.num_active]
|
|
383
|
+
state = self.transform.to_grid(active).unsqueeze(-1) * self.lift # [..., points, channels]
|
|
384
|
+
for layer in range(self.num_layers):
|
|
385
|
+
pointwise = torch.tensordot(weights, self.pointwise_stack[layer], dims=1)
|
|
386
|
+
multipliers = (torch.tensordot(weights, self.spectral_stack[layer], dims=1)
|
|
387
|
+
@ self.spectral_features.T) # [..., out, in, num_active]
|
|
388
|
+
modes = self.transform.from_grid(state.transpose(-1, -2))
|
|
389
|
+
spectral = self.transform.to_grid((multipliers * modes.unsqueeze(-3)).sum(-2))
|
|
390
|
+
biases = torch.tensordot(weights, self.bias_stack[layer], dims=1)
|
|
391
|
+
if context is not None and self.context_stack is not None:
|
|
392
|
+
biases = biases + combine_context(self.context_stack[layer], context, weights)
|
|
393
|
+
state = torch.tanh(torch.matmul(state, pointwise.transpose(-1, -2))
|
|
394
|
+
+ spectral.transpose(-1, -2) + biases.unsqueeze(-2))
|
|
395
|
+
projection = (weights @ self.project_stack).unsqueeze(-2)
|
|
396
|
+
update = self.transform.from_grid((state * projection).sum(-1))
|
|
397
|
+
if self.mode_scale is not None:
|
|
398
|
+
update = update * self.mode_scale[:self.num_active]
|
|
399
|
+
return torch.nn.functional.pad(update, (0, coeffs.shape[-1] - self.num_active))
|
|
400
|
+
|
|
401
|
+
def velocity_and_trace(self, coeffs, time_value, context=None):
|
|
402
|
+
return self.estimated_velocity_and_trace(coeffs, time_value, context)
|
|
403
|
+
|