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 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
+ ]
@@ -0,0 +1,4 @@
1
+ from pinnforge.auto.solver import AutoPINN, solve_pde
2
+ from pinnforge.auto.config import AutoConfig
3
+
4
+ __all__ = ["AutoPINN", "solve_pde", "AutoConfig"]
@@ -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
@@ -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,4 @@
1
+ from pinnforge.benchmarks.metrics import compute_relative_l2_error, compute_metrics
2
+ from pinnforge.benchmarks.runner import BenchmarkRunner, run_benchmark
3
+
4
+ __all__ = ["compute_relative_l2_error", "compute_metrics", "BenchmarkRunner", "run_benchmark"]
@@ -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