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.
Files changed (33) hide show
  1. FuncFlows/__init__.py +0 -0
  2. FuncFlows/base_measures/__init__.py +3 -0
  3. FuncFlows/base_measures/abstract_reference_measure.py +28 -0
  4. FuncFlows/base_measures/bases.py +112 -0
  5. FuncFlows/base_measures/gaussian_reference_measure.py +37 -0
  6. FuncFlows/diagnostics/__init__.py +2 -0
  7. FuncFlows/diagnostics/coverage.py +46 -0
  8. FuncFlows/diagnostics/importance.py +60 -0
  9. FuncFlows/objectives/__init__.py +5 -0
  10. FuncFlows/objectives/alpha_divergence.py +67 -0
  11. FuncFlows/objectives/flow_matching.py +132 -0
  12. FuncFlows/objectives/negative_logl.py +15 -0
  13. FuncFlows/objectives/reverse_kl.py +48 -0
  14. FuncFlows/samplers/__init__.py +1 -0
  15. FuncFlows/samplers/latent_pcn.py +53 -0
  16. FuncFlows/transports/__init__.py +3 -0
  17. FuncFlows/transports/abstract_transformation.py +32 -0
  18. FuncFlows/transports/continuous/__init__.py +6 -0
  19. FuncFlows/transports/continuous/base_continuous.py +63 -0
  20. FuncFlows/transports/continuous/conditioners.py +83 -0
  21. FuncFlows/transports/continuous/grid_fields.py +403 -0
  22. FuncFlows/transports/continuous/vector_fields.py +306 -0
  23. FuncFlows/transports/layers/__init__.py +2 -0
  24. FuncFlows/transports/layers/base_discrete.py +29 -0
  25. FuncFlows/transports/layers/layer_classes.py +81 -0
  26. FuncFlows/utils/__init__.py +0 -0
  27. FuncFlows/utils/gaussian_misfit.py +16 -0
  28. FuncFlows/utils/train.py +31 -0
  29. funcflows-0.1.2.dist-info/METADATA +43 -0
  30. funcflows-0.1.2.dist-info/RECORD +33 -0
  31. funcflows-0.1.2.dist-info/WHEEL +5 -0
  32. funcflows-0.1.2.dist-info/licenses/LICENSE +21 -0
  33. 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
+