skuld-lib 0.4.0__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.
- skuld/__init__.py +84 -0
- skuld/broknet.py +295 -0
- skuld/generators.py +574 -0
- skuld/kan.py +357 -0
- skuld/model.py +334 -0
- skuld/nni.py +184 -0
- skuld/scalers.py +84 -0
- skuld/siren.py +323 -0
- skuld/wire.py +318 -0
- skuld_lib-0.4.0.dist-info/METADATA +11 -0
- skuld_lib-0.4.0.dist-info/RECORD +13 -0
- skuld_lib-0.4.0.dist-info/WHEEL +5 -0
- skuld_lib-0.4.0.dist-info/top_level.txt +1 -0
skuld/__init__.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
##########################################################################
|
|
2
|
+
########################### SKULD NNI LIBRARY ############################
|
|
3
|
+
##########################################################################
|
|
4
|
+
### version 0.2.1 ###
|
|
5
|
+
### a neural network based ###
|
|
6
|
+
### numerical integration library ###
|
|
7
|
+
##########################################################################
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.1"
|
|
10
|
+
|
|
11
|
+
from .model import (
|
|
12
|
+
MLP,
|
|
13
|
+
init_model,
|
|
14
|
+
split_data,
|
|
15
|
+
set_global_device
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
from .scalers import (
|
|
19
|
+
scale_data,
|
|
20
|
+
descale_result
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from .nni import (
|
|
24
|
+
NeuralNumericalIntegration
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
from .generators import (
|
|
28
|
+
generate_data,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
from .siren import (
|
|
32
|
+
SirenLayer,
|
|
33
|
+
SirenPrimitiveNet,
|
|
34
|
+
SirenIntegrator,
|
|
35
|
+
mixed_partial_3,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
from .wire import (
|
|
39
|
+
WireLayer,
|
|
40
|
+
WireResidualBlock,
|
|
41
|
+
WirePrimitiveNet,
|
|
42
|
+
WireIntegrator,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
from .kan import (
|
|
46
|
+
BSplineBasis,
|
|
47
|
+
KANLayer,
|
|
48
|
+
KANPrimitiveNet,
|
|
49
|
+
KANIntegrator,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from .broknet import (
|
|
53
|
+
SindriNet,
|
|
54
|
+
BrokNet,
|
|
55
|
+
BrokNetIntegrator,
|
|
56
|
+
mixed_partial_3_expert,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"MLP",
|
|
61
|
+
"NeuralNumericalIntegration",
|
|
62
|
+
"init_model",
|
|
63
|
+
"split_data",
|
|
64
|
+
"scale_data",
|
|
65
|
+
"descale_result",
|
|
66
|
+
"generate_data",
|
|
67
|
+
"set_global_device",
|
|
68
|
+
"SirenLayer",
|
|
69
|
+
"SirenPrimitiveNet",
|
|
70
|
+
"SirenIntegrator",
|
|
71
|
+
"mixed_partial_3",
|
|
72
|
+
"WireLayer",
|
|
73
|
+
"WireResidualBlock",
|
|
74
|
+
"WirePrimitiveNet",
|
|
75
|
+
"WireIntegrator",
|
|
76
|
+
"BSplineBasis",
|
|
77
|
+
"KANLayer",
|
|
78
|
+
"KANPrimitiveNet",
|
|
79
|
+
"KANIntegrator",
|
|
80
|
+
"SindriNet",
|
|
81
|
+
"BrokNet",
|
|
82
|
+
"BrokNetIntegrator",
|
|
83
|
+
"mixed_partial_3_expert",
|
|
84
|
+
]
|
skuld/broknet.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
##############################################################################
|
|
2
|
+
# BrokNET: Mixture-of-Experts Neural Numerical Integration
|
|
3
|
+
#
|
|
4
|
+
# BrokNet is an ensemble of expert subnetworks (SindriNet), each trained
|
|
5
|
+
# on a single parameter set. A deterministic router dispatches inputs
|
|
6
|
+
# to the correct expert based on the discrete parameter label.
|
|
7
|
+
#
|
|
8
|
+
# Named after Brok and Sindri, the Huldra dwarves from God of War.
|
|
9
|
+
##############################################################################
|
|
10
|
+
import itertools
|
|
11
|
+
import math
|
|
12
|
+
import time
|
|
13
|
+
from typing import Callable, Dict, List, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import torch
|
|
17
|
+
import torch.nn as nn
|
|
18
|
+
|
|
19
|
+
from .siren import SirenLayer
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
##############################################################################
|
|
23
|
+
# SindriNet — Expert Subnetwork
|
|
24
|
+
##############################################################################
|
|
25
|
+
|
|
26
|
+
class SindriNet(nn.Module):
|
|
27
|
+
"""Expert subnetwork S_i(u1, u2, u3) — SIREN-based antiderivative approximator
|
|
28
|
+
for a single fixed parameter set (a_i, b_i, m_i, n_i).
|
|
29
|
+
|
|
30
|
+
Input: integration variables only (n_int_vars = 3 dimensions)
|
|
31
|
+
Output: scalar F_i(u) such that d^3 F_i / du1 du2 du3 ≈ f_tilde_i(u)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self,
|
|
35
|
+
n_int_vars: int = 3,
|
|
36
|
+
hidden_sizes: List[int] = None,
|
|
37
|
+
omega_0: float = 30.0,
|
|
38
|
+
output_scale: float = 1.0):
|
|
39
|
+
super().__init__()
|
|
40
|
+
if hidden_sizes is None:
|
|
41
|
+
hidden_sizes = [64, 64, 64]
|
|
42
|
+
|
|
43
|
+
self.n_int_vars = n_int_vars
|
|
44
|
+
self.omega_0 = omega_0
|
|
45
|
+
|
|
46
|
+
layers = []
|
|
47
|
+
in_dim = n_int_vars
|
|
48
|
+
for i, h in enumerate(hidden_sizes):
|
|
49
|
+
layers.append(SirenLayer(in_dim, h,
|
|
50
|
+
omega_0=omega_0,
|
|
51
|
+
is_first=(i == 0)))
|
|
52
|
+
in_dim = h
|
|
53
|
+
|
|
54
|
+
final = nn.Linear(in_dim, 1)
|
|
55
|
+
with torch.no_grad():
|
|
56
|
+
bound = math.sqrt(6.0 / in_dim) / omega_0
|
|
57
|
+
final.weight.uniform_(-bound, bound)
|
|
58
|
+
final.bias.uniform_(-bound, bound)
|
|
59
|
+
final.weight.data *= output_scale
|
|
60
|
+
final.bias.data *= output_scale
|
|
61
|
+
layers.append(final)
|
|
62
|
+
|
|
63
|
+
self.net = nn.Sequential(*layers)
|
|
64
|
+
|
|
65
|
+
def forward(self, u: torch.Tensor) -> torch.Tensor:
|
|
66
|
+
return self.net(u)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
##############################################################################
|
|
70
|
+
# BrokNet — Mixture-of-Experts Wrapper
|
|
71
|
+
##############################################################################
|
|
72
|
+
|
|
73
|
+
class BrokNet(nn.Module):
|
|
74
|
+
"""Brok — Mixture-of-Experts SIREN for parametric numerical integration.
|
|
75
|
+
|
|
76
|
+
Accepts full input (a, b, m, n, u1, u2, u3), routes by the discrete key
|
|
77
|
+
(a, b, m, n) to the corresponding SindriNet expert, which then operates
|
|
78
|
+
only on (u1, u2, u3).
|
|
79
|
+
|
|
80
|
+
The router is a plain dict — no learnable gating, no softmax.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self,
|
|
84
|
+
param_sets: List[Tuple],
|
|
85
|
+
n_int_vars: int = 3,
|
|
86
|
+
hidden_sizes: List[int] = None,
|
|
87
|
+
omega_0: float = 30.0,
|
|
88
|
+
output_scale: float = 1.0):
|
|
89
|
+
super().__init__()
|
|
90
|
+
if hidden_sizes is None:
|
|
91
|
+
hidden_sizes = [64, 64, 64]
|
|
92
|
+
|
|
93
|
+
self.param_sets = param_sets
|
|
94
|
+
self.n_int_vars = n_int_vars
|
|
95
|
+
self.n_params = 4
|
|
96
|
+
|
|
97
|
+
self.experts = nn.ModuleList([
|
|
98
|
+
SindriNet(n_int_vars=n_int_vars,
|
|
99
|
+
hidden_sizes=hidden_sizes,
|
|
100
|
+
omega_0=omega_0,
|
|
101
|
+
output_scale=output_scale)
|
|
102
|
+
for _ in param_sets
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
self.router: Dict[Tuple, int] = {
|
|
106
|
+
tuple(float(v) for v in ps): i
|
|
107
|
+
for i, ps in enumerate(param_sets)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
def expert_index(self, a: float, b: float, m: float, n: float) -> int:
|
|
111
|
+
key = (float(a), float(b), float(m), float(n))
|
|
112
|
+
if key not in self.router:
|
|
113
|
+
raise KeyError(
|
|
114
|
+
f"Parameter set {key} was not registered at construction time. "
|
|
115
|
+
f"Available sets: {list(self.router.keys())}"
|
|
116
|
+
)
|
|
117
|
+
return self.router[key]
|
|
118
|
+
|
|
119
|
+
def forward_expert(self, expert_idx: int, u: torch.Tensor) -> torch.Tensor:
|
|
120
|
+
return self.experts[expert_idx](u)
|
|
121
|
+
|
|
122
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
123
|
+
key = tuple(float(v) for v in x[0, :4].tolist())
|
|
124
|
+
idx = self.router[key]
|
|
125
|
+
u = x[:, 4:]
|
|
126
|
+
return self.experts[idx](u)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
##############################################################################
|
|
130
|
+
# Mixed Third Partial Derivative via Autograd
|
|
131
|
+
##############################################################################
|
|
132
|
+
|
|
133
|
+
def mixed_partial_3_expert(expert: SindriNet,
|
|
134
|
+
u: torch.Tensor) -> torch.Tensor:
|
|
135
|
+
"""Compute d^3 S_i / du1 du2 du3 for a single SindriNet expert."""
|
|
136
|
+
u_d = u.detach().requires_grad_(True)
|
|
137
|
+
G_out = expert(u_d).squeeze(-1)
|
|
138
|
+
ones = torch.ones_like(G_out)
|
|
139
|
+
|
|
140
|
+
g1 = torch.autograd.grad(
|
|
141
|
+
G_out, u_d, grad_outputs=ones,
|
|
142
|
+
create_graph=True, retain_graph=True,
|
|
143
|
+
)[0][:, 0]
|
|
144
|
+
|
|
145
|
+
g12 = torch.autograd.grad(
|
|
146
|
+
g1, u_d, grad_outputs=torch.ones_like(g1),
|
|
147
|
+
create_graph=True, retain_graph=True,
|
|
148
|
+
)[0][:, 1]
|
|
149
|
+
|
|
150
|
+
g123 = torch.autograd.grad(
|
|
151
|
+
g12, u_d, grad_outputs=torch.ones_like(g12),
|
|
152
|
+
create_graph=True,
|
|
153
|
+
)[0][:, 2]
|
|
154
|
+
|
|
155
|
+
return g123
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
##############################################################################
|
|
159
|
+
# BrokNetIntegrator: train + evaluate
|
|
160
|
+
##############################################################################
|
|
161
|
+
|
|
162
|
+
class BrokNetIntegrator:
|
|
163
|
+
"""Application of BrokNet for numerical integration (Maitre et al. approach).
|
|
164
|
+
|
|
165
|
+
Trains a BrokNet (mixture of SindriNet experts) to approximate the
|
|
166
|
+
antiderivative for each parameter set, then evaluates the integral
|
|
167
|
+
via an alternating-sign corner sum over the unit hypercube.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
def __init__(self,
|
|
171
|
+
param_sets: List[Tuple],
|
|
172
|
+
n_int_vars: int = 3,
|
|
173
|
+
hidden_sizes: List[int] = None,
|
|
174
|
+
omega_0: float = 30.0,
|
|
175
|
+
output_scale: float = 1.0):
|
|
176
|
+
self.param_sets = param_sets
|
|
177
|
+
self.n_int_vars = n_int_vars
|
|
178
|
+
self.n_params = 4
|
|
179
|
+
self.net = BrokNet(
|
|
180
|
+
param_sets=param_sets,
|
|
181
|
+
n_int_vars=n_int_vars,
|
|
182
|
+
hidden_sizes=hidden_sizes,
|
|
183
|
+
omega_0=omega_0,
|
|
184
|
+
output_scale=output_scale,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def n_weights(self) -> int:
|
|
189
|
+
return sum(p.numel() for p in self.net.parameters())
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def n_experts(self) -> int:
|
|
193
|
+
return len(self.param_sets)
|
|
194
|
+
|
|
195
|
+
def _center_value(self, integrand_fn, a, b, m, n) -> float:
|
|
196
|
+
uc = torch.full((1, self.n_int_vars), 0.5)
|
|
197
|
+
fc = integrand_fn(uc, a, b, m, n).item()
|
|
198
|
+
return fc if abs(fc) > 1e-30 else 1.0
|
|
199
|
+
|
|
200
|
+
def _make_expert_batch(self, integrand_fn, a, b, m, n, n_samples, device, norm_cache):
|
|
201
|
+
key = (float(a), float(b), float(m), float(n))
|
|
202
|
+
if key not in norm_cache:
|
|
203
|
+
norm_cache[key] = self._center_value(integrand_fn, a, b, m, n)
|
|
204
|
+
fc = norm_cache[key]
|
|
205
|
+
|
|
206
|
+
u = torch.rand(n_samples, self.n_int_vars)
|
|
207
|
+
f = integrand_fn(u, a, b, m, n) / fc
|
|
208
|
+
|
|
209
|
+
return u.to(device), f.to(device)
|
|
210
|
+
|
|
211
|
+
def train(self,
|
|
212
|
+
integrand_fn: Callable,
|
|
213
|
+
n_epochs: int = 8000,
|
|
214
|
+
n_per_expert: int = 1024,
|
|
215
|
+
lr: float = 5e-4,
|
|
216
|
+
device: torch.device = None,
|
|
217
|
+
verbose_every: int = 500) -> tuple:
|
|
218
|
+
"""Train BrokNet experts.
|
|
219
|
+
|
|
220
|
+
Returns: (loss_history, norm_cache)
|
|
221
|
+
"""
|
|
222
|
+
if device is None:
|
|
223
|
+
device = torch.device("cpu")
|
|
224
|
+
|
|
225
|
+
optimizer = torch.optim.Adam(self.net.parameters(), lr=lr)
|
|
226
|
+
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
|
|
227
|
+
optimizer, T_max=n_epochs, eta_min=lr / 10
|
|
228
|
+
)
|
|
229
|
+
loss_fn = nn.MSELoss()
|
|
230
|
+
self.net.to(device)
|
|
231
|
+
self.net.train()
|
|
232
|
+
|
|
233
|
+
n_experts = len(self.param_sets)
|
|
234
|
+
history = []
|
|
235
|
+
norm_cache: Dict = {}
|
|
236
|
+
t0 = time.time()
|
|
237
|
+
|
|
238
|
+
for epoch in range(1, n_epochs + 1):
|
|
239
|
+
optimizer.zero_grad()
|
|
240
|
+
expert_losses_tensors = []
|
|
241
|
+
|
|
242
|
+
for i, (a, b, m, n) in enumerate(self.param_sets):
|
|
243
|
+
u, f_tilde = self._make_expert_batch(
|
|
244
|
+
integrand_fn, a, b, m, n, n_per_expert, device, norm_cache
|
|
245
|
+
)
|
|
246
|
+
dG = mixed_partial_3_expert(self.net.experts[i], u)
|
|
247
|
+
loss_i = loss_fn(dG, f_tilde)
|
|
248
|
+
expert_losses_tensors.append(loss_i)
|
|
249
|
+
|
|
250
|
+
total_loss = torch.stack(expert_losses_tensors).mean()
|
|
251
|
+
total_loss.backward()
|
|
252
|
+
torch.nn.utils.clip_grad_norm_(self.net.parameters(), max_norm=5.0)
|
|
253
|
+
optimizer.step()
|
|
254
|
+
scheduler.step()
|
|
255
|
+
|
|
256
|
+
lv = total_loss.item()
|
|
257
|
+
history.append(lv)
|
|
258
|
+
|
|
259
|
+
if verbose_every > 0 and (epoch % verbose_every == 0 or epoch == 1):
|
|
260
|
+
lr_now = scheduler.get_last_lr()[0]
|
|
261
|
+
elapsed = time.time() - t0
|
|
262
|
+
print(f" Epoch {epoch:5d}/{n_epochs} "
|
|
263
|
+
f"loss={lv:.4e} lr={lr_now:.2e} "
|
|
264
|
+
f"({elapsed:.1f}s)")
|
|
265
|
+
|
|
266
|
+
return history, norm_cache
|
|
267
|
+
|
|
268
|
+
def integrate(self,
|
|
269
|
+
a: float, b: float, m: float, n: float,
|
|
270
|
+
norm_cache: dict = None,
|
|
271
|
+
device: torch.device = None) -> float:
|
|
272
|
+
"""Evaluate the integral for a given parameter set via corner sum."""
|
|
273
|
+
if device is None:
|
|
274
|
+
device = torch.device("cpu")
|
|
275
|
+
|
|
276
|
+
self.net.eval()
|
|
277
|
+
self.net.to(device)
|
|
278
|
+
|
|
279
|
+
key = (float(a), float(b), float(m), float(n))
|
|
280
|
+
if norm_cache and key in norm_cache:
|
|
281
|
+
fc = norm_cache[key]
|
|
282
|
+
else:
|
|
283
|
+
fc = 1.0
|
|
284
|
+
|
|
285
|
+
expert_idx = self.net.expert_index(a, b, m, n)
|
|
286
|
+
expert = self.net.experts[expert_idx]
|
|
287
|
+
|
|
288
|
+
I_tilde = 0.0
|
|
289
|
+
with torch.no_grad():
|
|
290
|
+
for corner in itertools.product([0.0, 1.0], repeat=self.n_int_vars):
|
|
291
|
+
sign = (-1) ** (self.n_int_vars - sum(corner))
|
|
292
|
+
u_t = torch.tensor([corner], dtype=torch.float32).to(device)
|
|
293
|
+
I_tilde += sign * expert(u_t).item()
|
|
294
|
+
|
|
295
|
+
return float(I_tilde * fc)
|