pinnforge 0.1.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.
- pinnforge/__init__.py +57 -0
- pinnforge/auto/__init__.py +4 -0
- pinnforge/auto/config.py +93 -0
- pinnforge/auto/solver.py +335 -0
- pinnforge/benchmarks/__init__.py +4 -0
- pinnforge/benchmarks/metrics.py +106 -0
- pinnforge/benchmarks/runner.py +237 -0
- pinnforge/core/__init__.py +4 -0
- pinnforge/core/pinn.py +423 -0
- pinnforge/core/solvers.py +119 -0
- pinnforge/examples/__init__.py +4 -0
- pinnforge/examples/burgers.py +142 -0
- pinnforge/examples/heat.py +152 -0
- pinnforge/physics/__init__.py +17 -0
- pinnforge/physics/pdes.py +115 -0
- pinnforge/symbolic/__init__.py +3 -0
- pinnforge/symbolic/pde.py +127 -0
- pinnforge/utils/__init__.py +17 -0
- pinnforge/utils/data.py +106 -0
- pinnforge/utils/logging.py +127 -0
- pinnforge-0.1.0.dist-info/METADATA +218 -0
- pinnforge-0.1.0.dist-info/RECORD +25 -0
- pinnforge-0.1.0.dist-info/WHEEL +5 -0
- pinnforge-0.1.0.dist-info/licenses/LICENSE +21 -0
- pinnforge-0.1.0.dist-info/top_level.txt +1 -0
pinnforge/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PINNForge - Physics-Informed Neural Networks made easy
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pinnforge.core.pinn import PINN, PINNTrainer, FourierFeatureMapping
|
|
6
|
+
from pinnforge.physics.pdes import BurgersEquation, HeatEquation, WaveEquation, PDE, create_pde
|
|
7
|
+
from pinnforge.utils.data import generate_simulation_data, add_noise
|
|
8
|
+
from pinnforge.utils.logging import ExperimentLogger
|
|
9
|
+
|
|
10
|
+
# Auto-solver
|
|
11
|
+
from pinnforge.auto import AutoPINN, solve_pde
|
|
12
|
+
|
|
13
|
+
# Symbolic
|
|
14
|
+
try:
|
|
15
|
+
from pinnforge.symbolic import SymbolicPDE, SymbolicPINNTrainer
|
|
16
|
+
except ImportError:
|
|
17
|
+
SymbolicPDE = None
|
|
18
|
+
|
|
19
|
+
# Benchmarks
|
|
20
|
+
try:
|
|
21
|
+
from pinnforge.benchmarks import compute_relative_l2_error, compute_metrics, BenchmarkRunner
|
|
22
|
+
except ImportError:
|
|
23
|
+
compute_relative_l2_error = None
|
|
24
|
+
|
|
25
|
+
__version__ = "0.1.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# One-line API
|
|
29
|
+
"solve_pde",
|
|
30
|
+
"AutoPINN",
|
|
31
|
+
|
|
32
|
+
# Core
|
|
33
|
+
"PINN",
|
|
34
|
+
"PINNTrainer",
|
|
35
|
+
"FourierFeatureMapping",
|
|
36
|
+
|
|
37
|
+
# Physics
|
|
38
|
+
"BurgersEquation",
|
|
39
|
+
"HeatEquation",
|
|
40
|
+
"WaveEquation",
|
|
41
|
+
"PDE",
|
|
42
|
+
"create_pde",
|
|
43
|
+
|
|
44
|
+
# Utils
|
|
45
|
+
"generate_simulation_data",
|
|
46
|
+
"add_noise",
|
|
47
|
+
"ExperimentLogger",
|
|
48
|
+
|
|
49
|
+
# Symbolic
|
|
50
|
+
"SymbolicPDE",
|
|
51
|
+
"SymbolicPINNTrainer",
|
|
52
|
+
|
|
53
|
+
# Benchmarks
|
|
54
|
+
"compute_relative_l2_error",
|
|
55
|
+
"compute_metrics",
|
|
56
|
+
"BenchmarkRunner",
|
|
57
|
+
]
|
pinnforge/auto/config.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-configuration heuristics for PINNs.
|
|
3
|
+
Based on PDE characteristics, auto-selects hyperparameters.
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Tuple, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class AutoConfig:
|
|
12
|
+
"""
|
|
13
|
+
Auto-selected configuration for a PINN solve.
|
|
14
|
+
"""
|
|
15
|
+
# Network
|
|
16
|
+
layers: List[int] = field(default_factory=lambda: [2, 64, 64, 64, 1])
|
|
17
|
+
activation: str = "tanh"
|
|
18
|
+
|
|
19
|
+
# Fourier features
|
|
20
|
+
use_fourier: bool = False
|
|
21
|
+
fourier_scale: float = 8.0
|
|
22
|
+
|
|
23
|
+
# Training
|
|
24
|
+
n_collocation: int = 10000
|
|
25
|
+
n_boundary: int = 200
|
|
26
|
+
n_initial: int = 200
|
|
27
|
+
epochs: int = 5000
|
|
28
|
+
learning_rate: float = 1e-3
|
|
29
|
+
batch_size: int = 1000
|
|
30
|
+
|
|
31
|
+
# Loss
|
|
32
|
+
use_adaptive_weights: bool = False
|
|
33
|
+
weight_bc: float = 1.0
|
|
34
|
+
weight_ic: float = 1.0
|
|
35
|
+
|
|
36
|
+
# Precision
|
|
37
|
+
precision: str = "float32"
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def for_pde(cls, pde_name: str, nu: Optional[float] = None,
|
|
41
|
+
domain: Optional[List[Tuple]] = None) -> "AutoConfig":
|
|
42
|
+
"""
|
|
43
|
+
Auto-select config based on PDE type and parameters.
|
|
44
|
+
|
|
45
|
+
Heuristics based on PINNacle findings and common failure modes:
|
|
46
|
+
- Stiff problems (low nu) need more collocation + larger Fourier scale
|
|
47
|
+
- High-frequency problems need wider networks
|
|
48
|
+
- Long time domains need more collocation points
|
|
49
|
+
"""
|
|
50
|
+
config = cls()
|
|
51
|
+
|
|
52
|
+
if pde_name.lower() == "burgers":
|
|
53
|
+
if nu is not None and nu < 0.02:
|
|
54
|
+
# Stiff Burgers: needs more points, larger Fourier scale
|
|
55
|
+
config.layers = [2, 128, 128, 128, 1]
|
|
56
|
+
config.fourier_scale = 15.0
|
|
57
|
+
config.n_collocation = 20000
|
|
58
|
+
config.epochs = 10000
|
|
59
|
+
config.learning_rate = 5e-4
|
|
60
|
+
else:
|
|
61
|
+
config.layers = [2, 64, 64, 64, 1]
|
|
62
|
+
config.fourier_scale = 10.0
|
|
63
|
+
|
|
64
|
+
elif pde_name.lower() == "heat":
|
|
65
|
+
config.layers = [2, 64, 64, 64, 1]
|
|
66
|
+
config.fourier_scale = 8.0
|
|
67
|
+
config.epochs = 3000
|
|
68
|
+
|
|
69
|
+
elif pde_name.lower() == "wave":
|
|
70
|
+
# Wave equations are high-frequency sensitive
|
|
71
|
+
config.layers = [2, 96, 96, 96, 1]
|
|
72
|
+
config.fourier_scale = 20.0
|
|
73
|
+
config.epochs = 8000
|
|
74
|
+
config.precision = "float64" # Waves need precision
|
|
75
|
+
|
|
76
|
+
elif pde_name.lower() == "navier-stokes":
|
|
77
|
+
# NS is the hardest: needs big network + precision
|
|
78
|
+
config.layers = [3, 128, 128, 128, 128, 2] # (x,y,t) -> (u,v)
|
|
79
|
+
config.fourier_scale = 12.0
|
|
80
|
+
config.n_collocation = 50000
|
|
81
|
+
config.epochs = 20000
|
|
82
|
+
config.learning_rate = 1e-4
|
|
83
|
+
config.precision = "float64"
|
|
84
|
+
|
|
85
|
+
# Domain-based adjustments
|
|
86
|
+
if domain:
|
|
87
|
+
# Longer time domains need more collocation
|
|
88
|
+
t_range = domain[1] if len(domain) > 1 else (0, 1)
|
|
89
|
+
t_span = t_range[1] - t_range[0]
|
|
90
|
+
if t_span > 2:
|
|
91
|
+
config.n_collocation = int(config.n_collocation * t_span / 1.0)
|
|
92
|
+
|
|
93
|
+
return config
|
pinnforge/auto/solver.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoPINN: One-function PDE solver.
|
|
3
|
+
"""
|
|
4
|
+
import torch
|
|
5
|
+
import numpy as np
|
|
6
|
+
from typing import Optional, List, Tuple, Dict, Any
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
from pinnforge.core.pinn import PINN, PINNTrainer
|
|
10
|
+
from pinnforge.physics.pdes import create_pde, PDE
|
|
11
|
+
from pinnforge.utils.data import (
|
|
12
|
+
create_collocation_points,
|
|
13
|
+
create_boundary_points,
|
|
14
|
+
create_initial_points,
|
|
15
|
+
)
|
|
16
|
+
from pinnforge.auto.config import AutoConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AutoPINN:
|
|
20
|
+
"""
|
|
21
|
+
Automatic PINN solver.
|
|
22
|
+
|
|
23
|
+
One call to solve() does everything:
|
|
24
|
+
1. Selects hyperparameters based on PDE characteristics
|
|
25
|
+
2. Generates collocation/boundary/initial points
|
|
26
|
+
3. Builds network with Fourier features
|
|
27
|
+
4. Trains with adaptive loss weighting
|
|
28
|
+
5. Validates against numerical solver
|
|
29
|
+
6. Generates report with plots and metrics
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> solver = AutoPINN("burgers", nu=0.01)
|
|
33
|
+
>>> result = solver.solve()
|
|
34
|
+
>>> print(f"Relative L2 error: {result['metrics']['relative_l2_error']:.2e}")
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
pde_name: str,
|
|
40
|
+
nu: Optional[float] = None,
|
|
41
|
+
domain: Optional[List[Tuple[float, float]]] = None,
|
|
42
|
+
config: Optional[AutoConfig] = None,
|
|
43
|
+
device: str = "cpu",
|
|
44
|
+
verbose: bool = True,
|
|
45
|
+
):
|
|
46
|
+
"""
|
|
47
|
+
Args:
|
|
48
|
+
pde_name: Name of PDE ('burgers', 'heat', 'wave', 'navier-stokes')
|
|
49
|
+
nu: Viscosity parameter (for Burgers)
|
|
50
|
+
domain: Domain specification [(x_min, x_max), (t_min, t_max)]
|
|
51
|
+
config: Override auto-config (optional)
|
|
52
|
+
device: 'cpu' or 'cuda'
|
|
53
|
+
verbose: Print progress
|
|
54
|
+
"""
|
|
55
|
+
self.pde_name = pde_name
|
|
56
|
+
self.nu = nu
|
|
57
|
+
self.domain = domain or [(0, 1), (0, 1)]
|
|
58
|
+
self.device = device
|
|
59
|
+
self.verbose = verbose
|
|
60
|
+
|
|
61
|
+
# Auto-select config if not provided
|
|
62
|
+
if config is None:
|
|
63
|
+
self.config = AutoConfig.for_pde(pde_name, nu=nu, domain=domain)
|
|
64
|
+
else:
|
|
65
|
+
self.config = config
|
|
66
|
+
|
|
67
|
+
# Create PDE
|
|
68
|
+
pde_kwargs = {}
|
|
69
|
+
if nu is not None:
|
|
70
|
+
pde_kwargs["nu"] = nu
|
|
71
|
+
self.pde = create_pde(pde_name, **pde_kwargs)
|
|
72
|
+
|
|
73
|
+
# Placeholders
|
|
74
|
+
self.model = None
|
|
75
|
+
self.trainer = None
|
|
76
|
+
self.result = None
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_symbolic(
|
|
80
|
+
cls,
|
|
81
|
+
equation,
|
|
82
|
+
independent_vars: List,
|
|
83
|
+
dependent_var,
|
|
84
|
+
domain: Optional[List[Tuple[float, float]]] = None,
|
|
85
|
+
device: str = "cpu",
|
|
86
|
+
**kwargs,
|
|
87
|
+
) -> "AutoPINN":
|
|
88
|
+
"""
|
|
89
|
+
Create AutoPINN from a SymPy equation.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> import sympy as sp
|
|
93
|
+
>>> x, t, u = sp.symbols('x t u')
|
|
94
|
+
>>> pde = sp.diff(u,t) + u*sp.diff(u,x) - 0.01*sp.diff(u,x,2)
|
|
95
|
+
>>> solver = AutoPINN.from_symbolic(pde, [x,t], u)
|
|
96
|
+
"""
|
|
97
|
+
from pinnforge.symbolic.pde import SymbolicPDE
|
|
98
|
+
|
|
99
|
+
symbolic_pde = SymbolicPDE(equation, dependent_var, independent_vars)
|
|
100
|
+
|
|
101
|
+
# Determine "name" for config selection
|
|
102
|
+
n_vars = len(independent_vars)
|
|
103
|
+
if n_vars == 2:
|
|
104
|
+
name = "symbolic_2d"
|
|
105
|
+
elif n_vars == 3:
|
|
106
|
+
name = "symbolic_3d"
|
|
107
|
+
else:
|
|
108
|
+
name = "symbolic"
|
|
109
|
+
|
|
110
|
+
# Create instance with symbolic PDE
|
|
111
|
+
instance = cls.__new__(cls)
|
|
112
|
+
instance.pde_name = name
|
|
113
|
+
instance.nu = None
|
|
114
|
+
instance.domain = domain or [(0, 1)] * n_vars
|
|
115
|
+
instance.device = device
|
|
116
|
+
instance.verbose = kwargs.get("verbose", True)
|
|
117
|
+
instance.pde = symbolic_pde
|
|
118
|
+
instance.config = AutoConfig.for_pde(name, domain=domain)
|
|
119
|
+
|
|
120
|
+
if "config" in kwargs:
|
|
121
|
+
instance.config = kwargs["config"]
|
|
122
|
+
|
|
123
|
+
instance.model = None
|
|
124
|
+
instance.trainer = None
|
|
125
|
+
instance.result = None
|
|
126
|
+
|
|
127
|
+
return instance
|
|
128
|
+
|
|
129
|
+
def solve(self, validate: bool = True) -> Dict[str, Any]:
|
|
130
|
+
"""
|
|
131
|
+
Solve the PDE. This is the main entry point.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Dictionary with:
|
|
135
|
+
- model: Trained PINN
|
|
136
|
+
- trainer: PINNTrainer
|
|
137
|
+
- history: Training history
|
|
138
|
+
- metrics: Validation metrics (if validate=True)
|
|
139
|
+
- solution: Callable for predictions
|
|
140
|
+
"""
|
|
141
|
+
if self.verbose:
|
|
142
|
+
print(f"[AutoPINN] Solving {self.pde_name} PDE")
|
|
143
|
+
print(f" Config: {self.config.layers}, Fourier scale={self.config.fourier_scale}")
|
|
144
|
+
print(f" Epochs: {self.config.epochs}, LR: {self.config.learning_rate}")
|
|
145
|
+
|
|
146
|
+
# 1. Build model
|
|
147
|
+
self.model = PINN(
|
|
148
|
+
layers=self.config.layers,
|
|
149
|
+
activation=self.config.activation,
|
|
150
|
+
use_fourier_features=self.config.use_fourier,
|
|
151
|
+
fourier_scale=self.config.fourier_scale,
|
|
152
|
+
use_adaptive_activation=False,
|
|
153
|
+
device=self.device,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
# 2. Generate data
|
|
157
|
+
x_pde, t_pde = create_collocation_points(
|
|
158
|
+
x_range=self.domain[0],
|
|
159
|
+
t_range=self.domain[1] if len(self.domain) > 1 else (0, 1),
|
|
160
|
+
n_points=self.config.n_collocation,
|
|
161
|
+
device=self.device,
|
|
162
|
+
)
|
|
163
|
+
x_bc, t_bc = create_boundary_points(
|
|
164
|
+
x_range=self.domain[0],
|
|
165
|
+
t_range=self.domain[1] if len(self.domain) > 1 else (0, 1),
|
|
166
|
+
n_points=self.config.n_boundary,
|
|
167
|
+
device=self.device,
|
|
168
|
+
)
|
|
169
|
+
bc_values = self.pde.boundary_condition(x_bc, t_bc)
|
|
170
|
+
x_ic, t_ic = create_initial_points(
|
|
171
|
+
x_range=self.domain[0],
|
|
172
|
+
n_points=self.config.n_initial,
|
|
173
|
+
device=self.device,
|
|
174
|
+
)
|
|
175
|
+
ic_values = self.pde.initial_condition(x_ic)
|
|
176
|
+
|
|
177
|
+
# 3. Train
|
|
178
|
+
self.trainer = PINNTrainer(
|
|
179
|
+
model=self.model,
|
|
180
|
+
pde_fn=self.pde.residual,
|
|
181
|
+
learning_rate=self.config.learning_rate,
|
|
182
|
+
use_adaptive_weights=self.config.use_adaptive_weights,
|
|
183
|
+
device=self.device,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
history = self.trainer.train(
|
|
187
|
+
x_pde=x_pde, t_pde=t_pde,
|
|
188
|
+
x_bc=x_bc, t_bc=t_bc, bc_values=bc_values,
|
|
189
|
+
x_ic=x_ic, t_ic=t_ic, ic_values=ic_values,
|
|
190
|
+
epochs=self.config.epochs,
|
|
191
|
+
batch_size=self.config.batch_size,
|
|
192
|
+
weights={"pde": 1.0, "bc": self.config.weight_bc, "ic": self.config.weight_ic},
|
|
193
|
+
verbose=self.verbose,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# 4. Validate (optional)
|
|
197
|
+
metrics = {}
|
|
198
|
+
if validate and hasattr(self.pde, 'analytical_solution'):
|
|
199
|
+
metrics = self._validate()
|
|
200
|
+
|
|
201
|
+
# 5. Build result
|
|
202
|
+
self.result = {
|
|
203
|
+
"model": self.model,
|
|
204
|
+
"trainer": self.trainer,
|
|
205
|
+
"history": history,
|
|
206
|
+
"metrics": metrics,
|
|
207
|
+
"config": self.config,
|
|
208
|
+
"predict": lambda x, t: self.trainer.predict(
|
|
209
|
+
torch.tensor(x, dtype=torch.float32, device=self.device).reshape(-1, 1),
|
|
210
|
+
torch.tensor(t, dtype=torch.float32, device=self.device).reshape(-1, 1),
|
|
211
|
+
),
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if self.verbose:
|
|
215
|
+
print(f"[AutoPINN] Complete.")
|
|
216
|
+
if metrics:
|
|
217
|
+
print(f" Relative L2 error: {metrics.get('relative_l2_error', 'N/A'):.4e}")
|
|
218
|
+
|
|
219
|
+
return self.result
|
|
220
|
+
|
|
221
|
+
def _validate(self) -> Dict[str, float]:
|
|
222
|
+
"""Validate against analytical solution."""
|
|
223
|
+
from pinnforge.benchmarks.metrics import compute_metrics
|
|
224
|
+
|
|
225
|
+
# Dense test grid
|
|
226
|
+
n_test = 5000
|
|
227
|
+
x_test = torch.linspace(
|
|
228
|
+
self.domain[0][0], self.domain[0][1], int(np.sqrt(n_test)),
|
|
229
|
+
device=self.device
|
|
230
|
+
).reshape(-1, 1)
|
|
231
|
+
t_test = torch.linspace(
|
|
232
|
+
self.domain[1][0], self.domain[1][1], int(np.sqrt(n_test)),
|
|
233
|
+
device=self.device
|
|
234
|
+
).reshape(-1, 1)
|
|
235
|
+
|
|
236
|
+
X, T = torch.meshgrid(x_test.squeeze(), t_test.squeeze(), indexing='ij')
|
|
237
|
+
X_flat = X.reshape(-1, 1)
|
|
238
|
+
T_flat = T.reshape(-1, 1)
|
|
239
|
+
|
|
240
|
+
with torch.no_grad():
|
|
241
|
+
u_pred = self.trainer.predict(X_flat, T_flat)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
u_true = self.pde.analytical_solution(X_flat, T_flat)
|
|
245
|
+
return compute_metrics(u_pred, u_true)
|
|
246
|
+
except (NotImplementedError, AttributeError):
|
|
247
|
+
# No analytical solution available, skip validation
|
|
248
|
+
return {}
|
|
249
|
+
|
|
250
|
+
def report(self, save_path: Optional[str] = None):
|
|
251
|
+
"""
|
|
252
|
+
Generate a visual report of the solution.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
save_path: Path to save the report image. If None, displays.
|
|
256
|
+
"""
|
|
257
|
+
if self.result is None:
|
|
258
|
+
raise RuntimeError("Must call solve() before report()")
|
|
259
|
+
|
|
260
|
+
import matplotlib.pyplot as plt
|
|
261
|
+
|
|
262
|
+
# Create dense grid
|
|
263
|
+
n = 100
|
|
264
|
+
x = torch.linspace(self.domain[0][0], self.domain[0][1], n, device=self.device)
|
|
265
|
+
t = torch.linspace(
|
|
266
|
+
self.domain[1][0] if len(self.domain) > 1 else 0,
|
|
267
|
+
self.domain[1][1] if len(self.domain) > 1 else 1,
|
|
268
|
+
n, device=self.device
|
|
269
|
+
)
|
|
270
|
+
X, T = torch.meshgrid(x, t, indexing='ij')
|
|
271
|
+
X_flat = X.reshape(-1, 1)
|
|
272
|
+
T_flat = T.reshape(-1, 1)
|
|
273
|
+
|
|
274
|
+
with torch.no_grad():
|
|
275
|
+
u_pred = self.trainer.predict(X_flat, T_flat).reshape(n, n)
|
|
276
|
+
|
|
277
|
+
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
|
|
278
|
+
|
|
279
|
+
# Predicted solution
|
|
280
|
+
im1 = axes[0].contourf(X.cpu().numpy(), T.cpu().numpy(), u_pred.cpu().numpy(), levels=50, cmap='viridis')
|
|
281
|
+
axes[0].set_xlabel('x'); axes[0].set_ylabel('t')
|
|
282
|
+
axes[0].set_title('PINN Solution')
|
|
283
|
+
plt.colorbar(im1, ax=axes[0])
|
|
284
|
+
|
|
285
|
+
# Training loss
|
|
286
|
+
if 'loss_total' in self.result['history']:
|
|
287
|
+
axes[1].semilogy(self.result['history']['loss_total'])
|
|
288
|
+
axes[1].set_xlabel('Epoch'); axes[1].set_ylabel('Loss')
|
|
289
|
+
axes[1].set_title('Training Convergence')
|
|
290
|
+
axes[1].grid(True, alpha=0.3)
|
|
291
|
+
|
|
292
|
+
# Error (if analytical available)
|
|
293
|
+
if 'relative_l2_error' in self.result['metrics']:
|
|
294
|
+
u_true = self.pde.analytical_solution(X_flat, T_flat).reshape(n, n)
|
|
295
|
+
error = torch.abs(u_pred - u_true)
|
|
296
|
+
im3 = axes[2].contourf(X.cpu().numpy(), T.cpu().numpy(), error.cpu().numpy(), levels=50, cmap='hot')
|
|
297
|
+
axes[2].set_xlabel('x'); axes[2].set_ylabel('t')
|
|
298
|
+
axes[2].set_title(f'Error (max: {error.max():.2e})')
|
|
299
|
+
plt.colorbar(im3, ax=axes[2])
|
|
300
|
+
|
|
301
|
+
plt.tight_layout()
|
|
302
|
+
if save_path:
|
|
303
|
+
plt.savefig(save_path, dpi=150)
|
|
304
|
+
print(f"[AutoPINN] Report saved to {save_path}")
|
|
305
|
+
else:
|
|
306
|
+
plt.show()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def solve_pde(
|
|
310
|
+
pde_name: str,
|
|
311
|
+
nu: Optional[float] = None,
|
|
312
|
+
domain: Optional[List[Tuple[float, float]]] = None,
|
|
313
|
+
epochs: Optional[int] = None,
|
|
314
|
+
device: str = "cpu",
|
|
315
|
+
verbose: bool = True,
|
|
316
|
+
) -> Dict[str, Any]:
|
|
317
|
+
"""
|
|
318
|
+
One-line PDE solver.
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
>>> result = solve_pde("burgers", nu=0.01, epochs=5000)
|
|
322
|
+
>>> print(result['metrics']['relative_l2_error'])
|
|
323
|
+
"""
|
|
324
|
+
solver = AutoPINN(
|
|
325
|
+
pde_name=pde_name,
|
|
326
|
+
nu=nu,
|
|
327
|
+
domain=domain,
|
|
328
|
+
device=device,
|
|
329
|
+
verbose=verbose,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
if epochs is not None:
|
|
333
|
+
solver.config.epochs = epochs
|
|
334
|
+
|
|
335
|
+
return solver.solve()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Benchmarking metrics for PINN evaluation.
|
|
3
|
+
Pure-PyTorch implementation, no external ML dependencies.
|
|
4
|
+
"""
|
|
5
|
+
import torch
|
|
6
|
+
from typing import Dict, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compute_relative_l2_error(
|
|
10
|
+
u_pred: torch.Tensor,
|
|
11
|
+
u_true: torch.Tensor,
|
|
12
|
+
) -> float:
|
|
13
|
+
"""
|
|
14
|
+
Compute relative L2 error.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
u_pred: Predicted solution
|
|
18
|
+
u_true: Ground truth solution
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Relative L2 error
|
|
22
|
+
"""
|
|
23
|
+
u_pred = u_pred.flatten()
|
|
24
|
+
u_true = u_true.flatten()
|
|
25
|
+
|
|
26
|
+
numerator = torch.norm(u_pred - u_true, p=2)
|
|
27
|
+
denominator = torch.norm(u_true, p=2)
|
|
28
|
+
|
|
29
|
+
if denominator == 0:
|
|
30
|
+
return 0.0
|
|
31
|
+
|
|
32
|
+
return (numerator / denominator).item()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def compute_metrics(
|
|
36
|
+
u_pred: torch.Tensor,
|
|
37
|
+
u_true: torch.Tensor,
|
|
38
|
+
) -> Dict[str, float]:
|
|
39
|
+
"""
|
|
40
|
+
Compute comprehensive set of metrics.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary with: relative_l2_error, mse, rmse, r2_score, max_error, mean_error
|
|
44
|
+
"""
|
|
45
|
+
u_pred = u_pred.flatten().detach()
|
|
46
|
+
u_true = u_true.flatten().detach()
|
|
47
|
+
|
|
48
|
+
# Mean squared error
|
|
49
|
+
mse = torch.mean((u_pred - u_true) ** 2).item()
|
|
50
|
+
|
|
51
|
+
# Root mean squared error
|
|
52
|
+
rmse = mse ** 0.5
|
|
53
|
+
|
|
54
|
+
# R^2 score: 1 - SS_res / SS_tot
|
|
55
|
+
ss_res = torch.sum((u_true - u_pred) ** 2)
|
|
56
|
+
ss_tot = torch.sum((u_true - u_true.mean()) ** 2)
|
|
57
|
+
if ss_tot.item() == 0:
|
|
58
|
+
r2 = 0.0
|
|
59
|
+
else:
|
|
60
|
+
r2 = (1.0 - ss_res / ss_tot).item()
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
"relative_l2_error": compute_relative_l2_error(u_pred, u_true),
|
|
64
|
+
"mse": mse,
|
|
65
|
+
"rmse": rmse,
|
|
66
|
+
"r2_score": r2,
|
|
67
|
+
"max_error": torch.max(torch.abs(u_pred - u_true)).item(),
|
|
68
|
+
"mean_error": torch.mean(torch.abs(u_pred - u_true)).item(),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def compute_convergence_rate(
|
|
73
|
+
errors: List[float],
|
|
74
|
+
iterations: List[int],
|
|
75
|
+
) -> float:
|
|
76
|
+
"""
|
|
77
|
+
Estimate convergence rate from error history.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
errors: List of errors over iterations
|
|
81
|
+
iterations: Corresponding iteration numbers
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Convergence rate (higher is better)
|
|
85
|
+
"""
|
|
86
|
+
if len(errors) < 2:
|
|
87
|
+
return 0.0
|
|
88
|
+
|
|
89
|
+
import math
|
|
90
|
+
|
|
91
|
+
# Fit power law: error = C * iter^(-rate)
|
|
92
|
+
log_errors = [math.log(e) for e in errors]
|
|
93
|
+
log_iters = [math.log(i) for i in iterations]
|
|
94
|
+
|
|
95
|
+
n = len(log_errors)
|
|
96
|
+
mean_x = sum(log_iters) / n
|
|
97
|
+
mean_y = sum(log_errors) / n
|
|
98
|
+
|
|
99
|
+
num = sum((log_iters[i] - mean_x) * (log_errors[i] - mean_y) for i in range(n))
|
|
100
|
+
den = sum((log_iters[i] - mean_x) ** 2 for i in range(n))
|
|
101
|
+
|
|
102
|
+
if den == 0:
|
|
103
|
+
return 0.0
|
|
104
|
+
|
|
105
|
+
slope = num / den
|
|
106
|
+
return -slope
|