Skip to content

Commit 3972356

Browse files
0.29.13
1 parent fd76822 commit 3972356

7 files changed

Lines changed: 734 additions & 1 deletion

File tree

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "spotpython"
10-
version = "0.29.12"
10+
version = "0.29.13"
1111
authors = [
1212
{ name="T. Bartz-Beielstein", email="tbb@bartzundbartz.de" }
1313
]
@@ -42,6 +42,8 @@ dependencies = [
4242
"nbformat",
4343
"pandas",
4444
"plotly",
45+
"pytest",
46+
"pytest-mock",
4547
"PyQt6",
4648
"python-markdown-math",
4749
"pytorch-lightning>=1.4",

src/spotpython/pinns/nn/fcn.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import torch
2+
import torch.nn as nn
3+
4+
5+
class FCN(nn.Module):
6+
"""A Fully Connected Network (FCN).
7+
8+
This network consists of an input layer, a specified number of hidden layers,
9+
and an output layer. All hidden layers use the Tanh activation function.
10+
11+
Attributes:
12+
fcs (nn.Sequential): Sequential container for the first linear layer
13+
(input to hidden) and its activation.
14+
fch (nn.Sequential): Sequential container for the hidden layers. Each hidden
15+
layer consists of a linear transformation and an activation.
16+
fce (nn.Linear): The final linear layer (hidden to output).
17+
18+
References:
19+
- Solving differential equations using physics informed deep learning: a hand-on tutorial with benchmark tests. Baty, Hubert and Baty, Leo. April 2023.
20+
"""
21+
22+
def __init__(self, N_INPUT: int, N_OUTPUT: int, N_HIDDEN: int, N_LAYERS: int):
23+
"""Initializes the FCN.
24+
25+
Args:
26+
N_INPUT (int): The number of input features.
27+
N_OUTPUT (int): The number of output features.
28+
N_HIDDEN (int): The number of neurons in each hidden layer.
29+
N_LAYERS (int): The total number of layers, including the input layer
30+
(which is N_INPUT -> N_HIDDEN), hidden layers, but
31+
not counting the final output layer transformation.
32+
A N_LAYERS=1 means only input to hidden, then hidden to output.
33+
A N_LAYERS=2 means input to hidden, one hidden to hidden, then hidden to output.
34+
The number of hidden-to-hidden layers is N_LAYERS - 1.
35+
If N_LAYERS is 1, there are no fch layers.
36+
37+
Examples:
38+
>>> # Example of creating a FCN
39+
>>> from spotpython.pinns.nn.fcn import FCN
40+
>>> model = FCN(N_INPUT=1, N_OUTPUT=1, N_HIDDEN=10, N_LAYERS=3)
41+
>>> print(model)
42+
FCN(
43+
(fcs): Sequential(
44+
(0): Linear(in_features=1, out_features=10, bias=True)
45+
(1): Tanh()
46+
)
47+
(fch): Sequential(
48+
(0): Sequential(
49+
(0): Linear(in_features=10, out_features=10, bias=True)
50+
(1): Tanh()
51+
)
52+
(1): Sequential(
53+
(0): Linear(in_features=10, out_features=10, bias=True)
54+
(1): Tanh()
55+
)
56+
)
57+
(fce): Linear(in_features=10, out_features=1, bias=True)
58+
)
59+
>>> # Example of a forward pass
60+
>>> input_tensor = torch.randn(5, 1) # Batch of 5, 1 input feature
61+
>>> output_tensor = model(input_tensor)
62+
>>> print(output_tensor.shape)
63+
torch.Size([5, 1])
64+
65+
>>> # Example with N_LAYERS = 1 (no hidden-to-hidden layers)
66+
>>> model_simple = FCN(N_INPUT=2, N_OUTPUT=1, N_HIDDEN=5, N_LAYERS=1)
67+
>>> print(model_simple)
68+
FCN(
69+
(fcs): Sequential(
70+
(0): Linear(in_features=2, out_features=5, bias=True)
71+
(1): Tanh()
72+
)
73+
(fch): Sequential()
74+
(fce): Linear(in_features=5, out_features=1, bias=True)
75+
)
76+
"""
77+
super().__init__()
78+
activation = nn.Tanh
79+
80+
# Input layer: N_INPUT -> N_HIDDEN
81+
self.fcs = nn.Sequential(nn.Linear(N_INPUT, N_HIDDEN), activation())
82+
83+
# Hidden layers: N_HIDDEN -> N_HIDDEN, (N_LAYERS - 1) times
84+
# If N_LAYERS is 1, range(N_LAYERS-1) is range(0), so fch will be empty.
85+
hidden_layers = []
86+
if N_LAYERS > 1:
87+
for _ in range(N_LAYERS - 1):
88+
hidden_layers.append(nn.Sequential(nn.Linear(N_HIDDEN, N_HIDDEN), activation()))
89+
self.fch = nn.Sequential(*hidden_layers)
90+
91+
# Output layer: N_HIDDEN -> N_OUTPUT
92+
self.fce = nn.Linear(N_HIDDEN, N_OUTPUT)
93+
94+
def forward(self, x: torch.Tensor) -> torch.Tensor:
95+
"""Performs the forward pass of the network.
96+
97+
Args:
98+
x (torch.Tensor): The input tensor. Shape (batch_size, N_INPUT).
99+
100+
Returns:
101+
torch.Tensor: The output tensor. Shape (batch_size, N_OUTPUT).
102+
"""
103+
x = self.fcs(x)
104+
x = self.fch(x)
105+
x = self.fce(x)
106+
return x
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import torch
2+
import matplotlib.pyplot as plt
3+
from typing import Optional, Union, List
4+
import numpy as np
5+
6+
7+
def plot_result(
8+
x: Union[torch.Tensor, List[float], "np.ndarray"],
9+
y: Union[torch.Tensor, List[float], "np.ndarray"],
10+
x_data: Union[torch.Tensor, List[float], "np.ndarray"],
11+
y_data: Union[torch.Tensor, List[float], "np.ndarray"],
12+
yh: Union[torch.Tensor, List[float], "np.ndarray"],
13+
current_step: int,
14+
xp: Optional[Union[torch.Tensor, List[float], "np.ndarray"]] = None,
15+
figure_size: tuple = (8, 4),
16+
xlims: Optional[tuple] = (-1.25, 31.05),
17+
ylims: Optional[tuple] = (-0.65, 2.25),
18+
show_plot: bool = True,
19+
save_path: Optional[str] = None,
20+
) -> None:
21+
"""Plots the results of a PINN training, comparing predictions with exact solutions.
22+
23+
Displays the neural network's prediction, the exact solution, training data points,
24+
and optionally, collocation points.
25+
26+
Args:
27+
x (Union[torch.Tensor, List[float], "np.ndarray"]):
28+
The x-coordinates for the continuous plots (e.g., time points).
29+
y (Union[torch.Tensor, List[float], "np.ndarray"]):
30+
The y-coordinates of the exact solution corresponding to `x`.
31+
x_data (Union[torch.Tensor, List[float], "np.ndarray"]):
32+
The x-coordinates of the training data points.
33+
y_data (Union[torch.Tensor, List[float], "np.ndarray"]):
34+
The y-coordinates of the training data points.
35+
yh (Union[torch.Tensor, List[float], "np.ndarray"]):
36+
The y-coordinates of the neural network's prediction corresponding to `x`.
37+
current_step (int):
38+
The current training step or epoch number to display on the plot.
39+
xp (Optional[Union[torch.Tensor, List[float], "np.ndarray"]], optional):
40+
The x-coordinates of the collocation points. If None, these are not plotted.
41+
Defaults to None.
42+
figure_size (tuple, optional):
43+
Size of the matplotlib figure. Defaults to (8, 4).
44+
xlims (Optional[tuple], optional):
45+
Tuple defining the x-axis limits. If None, matplotlib's default is used.
46+
Defaults to (-1.25, 31.05).
47+
ylims (Optional[tuple], optional):
48+
Tuple defining the y-axis limits. If None, matplotlib's default is used.
49+
Defaults to (-0.65, 2.25).
50+
show_plot (bool, optional):
51+
Whether to display the plot using `plt.show()`. Defaults to True.
52+
save_path (Optional[str], optional):
53+
If provided, the path to save the figure to. If None, the figure is not saved.
54+
Defaults to None.
55+
56+
Examples:
57+
>>> from spotpython.pinns.plot.result import plot_result
58+
>>> import torch
59+
>>> import numpy as np
60+
>>> # Generate some dummy data
61+
>>> x_plot = torch.linspace(0, 30, 100)
62+
>>> y_exact = torch.sin(x_plot / 5)
63+
>>> y_pred = torch.sin(x_plot / 5 + 0.1) # Slightly off prediction
64+
>>> x_train = torch.rand(10) * 30
65+
>>> y_train = torch.sin(x_train / 5)
66+
>>> collocation_points = torch.rand(50) * 30
67+
>>> current_training_step = 1000
68+
>>> # plot_result( # This would show a plot if run in an interactive environment
69+
... # x_plot, y_exact, x_train, y_train, y_pred,
70+
... # current_training_step, xp=collocation_points,
71+
... # show_plot=False, save_path="temp_plot.png"
72+
... # )
73+
>>> # To avoid actual plotting in doctest, we'll just confirm it runs
74+
>>> try:
75+
... plot_result(
76+
... x_plot.numpy(), y_exact.numpy(), x_train.numpy(), y_train.numpy(), y_pred.numpy(),
77+
... current_training_step, xp=collocation_points.numpy(),
78+
... show_plot=False
79+
... )
80+
... except Exception as e:
81+
... print(f"Plotting failed: {e}")
82+
83+
Note:
84+
If using PyTorch tensors as input, they will be detached and moved to CPU
85+
before plotting. Consider converting to NumPy arrays beforehand if preferred.
86+
87+
References:
88+
- Solving differential equations using physics informed deep learning: a hand-on tutorial with benchmark tests. Baty, Hubert and Baty, Leo. April 2023.
89+
"""
90+
91+
# Convert tensors to numpy arrays for plotting if they are tensors
92+
def to_numpy(data):
93+
if isinstance(data, torch.Tensor):
94+
return data.detach().cpu().numpy()
95+
return data
96+
97+
x_np = to_numpy(x)
98+
y_np = to_numpy(y)
99+
x_data_np = to_numpy(x_data)
100+
y_data_np = to_numpy(y_data)
101+
yh_np = to_numpy(yh)
102+
if xp is not None:
103+
xp_np = to_numpy(xp)
104+
else:
105+
xp_np = None
106+
107+
plt.figure(figsize=figure_size)
108+
plt.plot(x_np, yh_np, color="tab:red", linewidth=2, alpha=0.8, label="NN prediction")
109+
plt.plot(x_np, y_np, color="blue", linewidth=2, alpha=0.8, linestyle="--", label="Exact solution")
110+
plt.scatter(x_data_np, y_data_np, s=60, color="tab:red", alpha=0.4, label="Training data")
111+
112+
if xp_np is not None:
113+
# Create y-values for collocation points at y=0 or a specified level
114+
# Original code used -0*torch.ones_like(xp), which is just zeros.
115+
xp_y_values = np.zeros_like(xp_np)
116+
plt.scatter(xp_np, xp_y_values, s=30, color="tab:green", alpha=0.4, label="Collocation points")
117+
118+
legend_handle = plt.legend(loc=(0.67, 0.62), frameon=False, fontsize="large")
119+
plt.setp(legend_handle.get_texts(), color="k")
120+
121+
if xlims:
122+
plt.xlim(xlims)
123+
if ylims:
124+
plt.ylim(ylims)
125+
126+
plt.text(0.05, 0.95, f"Training step: {current_step}", fontsize="xx-large", color="k", transform=plt.gca().transAxes, ha="left", va="top")
127+
128+
plt.ylabel("y", fontsize="xx-large")
129+
plt.xlabel("Time", fontsize="xx-large")
130+
plt.axis("on")
131+
plt.grid(True, linestyle="--", alpha=0.7)
132+
133+
if save_path:
134+
plt.savefig(save_path, dpi=300, bbox_inches="tight")
135+
136+
if show_plot:
137+
plt.show()
138+
else:
139+
plt.close() # Close the figure if not shown to free memory

src/spotpython/pinns/solvers.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import numpy as np
2+
import torch
3+
from typing import Tuple
4+
5+
6+
def oscillatorb(n_steps: int = 3000, t_min: float = 0.0, t_max: float = 30.0, y0: float = 1.0, alpha: float = 0.1, omega: float = np.pi / 2) -> Tuple[torch.Tensor, torch.Tensor]:
7+
"""Solves the first-order ODE y' = -alpha*y + sin(omega*t) using a
8+
two-stage explicit Runge-Kutta method as described in the reference.
9+
The ODE represents a damped harmonic oscillator with a sine forcing term.
10+
11+
The specific numerical scheme used is:
12+
1. y_intermediate = y_current + (t_step/2) * f(t_current + t_step/2, y_current)
13+
2. y_next = y_current + t_step * f(t_current + t_step, y_intermediate)
14+
where f(t,y) = -alpha*y + sin(omega*t). This is a second-order method.
15+
16+
Args:
17+
n_steps (int): Number of time points in the discretized time domain,
18+
including the initial point. Defaults to 3000.
19+
t_min (float): Initial time. Defaults to 0.0.
20+
t_max (float): Defines the nominal end of the time interval. The time step
21+
is calculated as (t_max - t_min) / n_steps. The actual last
22+
time point will be t_min + (n_steps - 1) * t_step. Defaults to 30.0.
23+
y0 (float): Initial condition for y at t_min. Defaults to 1.0.
24+
alpha (float): Damping coefficient in the ODE. Defaults to 0.1.
25+
omega (float): Angular frequency for the sine forcing term in the ODE.
26+
Defaults to np.pi / 2.
27+
28+
Returns:
29+
Tuple[torch.Tensor, torch.Tensor]: A tuple containing two PyTorch tensors:
30+
- t_points_tensor: Tensor of time points, shape (n_steps, 1).
31+
- y_values_tensor: Tensor of corresponding y values, shape (n_steps, 1).
32+
33+
Examples:
34+
>>> from spotpython.pinns.solvers import oscillatorb
35+
>>> import numpy as np
36+
>>> import torch
37+
>>> t_vals, y_vals = oscillatorb(n_steps=100, t_min=0.0, t_max=10.0, y0=1.0, alpha=0.1, omega=np.pi/2)
38+
>>> print(t_vals.shape, y_vals.shape)
39+
torch.Size([100, 1]) torch.Size([100, 1])
40+
>>> print(f"Initial t: {t_vals[0].item():.2f}, Initial y: {y_vals[0].item():.2f}")
41+
Initial t: 0.00, Initial y: 1.00
42+
>>> # Last t will be t_max - t_step for this configuration
43+
>>> # t_step = (10.0 - 0.0) / 100 = 0.1
44+
>>> # Last t = 0.0 + (100-1)*0.1 = 9.9
45+
>>> print(f"Final t: {t_vals[-1].item():.2f}, Final y: {y_vals[-1].item():.2f}")
46+
Final t: 9.90, Final y: ...
47+
48+
References:
49+
- Solving differential equations using physics informed deep learning: a hand-on tutorial with benchmark tests. Baty, Hubert and Baty, Leo. April 2023.
50+
"""
51+
t_step = (t_max - t_min) / n_steps # Time step
52+
# Time points: t_min, t_min + t_step, ..., t_min + (n_steps-1)*t_step
53+
t_points = np.arange(t_min, t_min + n_steps * t_step, t_step)[:n_steps]
54+
55+
y = [y0] # List to store y values, starting with initial condition
56+
57+
# Solve for the time evolution
58+
# t_points[0] corresponds to y0. Loop starts from t_points[1].
59+
for t_current_step_end in t_points[1:]:
60+
# t_midpoint is the midpoint of the current integration interval
61+
# Interval: [t_current_step_end - t_step, t_current_step_end]
62+
# Midpoint: (t_current_step_end - t_step) + t_step/2 = t_current_step_end - t_step/2
63+
t_midpoint = t_current_step_end - t_step / 2.0
64+
# y_prev is the last computed value of y
65+
y_prev = y[-1]
66+
67+
# Stage 1: Calculate intermediate y value (y_intermediate)
68+
# Uses slope at t_midpoint, with y_prev
69+
# f(t,y) = -alpha*y + sin(omega*t)
70+
slope_at_t_mid_using_y_prev = -alpha * y_prev + np.sin(omega * t_midpoint)
71+
y_intermediate = y_prev + (t_step / 2.0) * slope_at_t_mid_using_y_prev
72+
73+
# Stage 2: Calculate y at t_current_step_end
74+
# Uses slope at t_current_step_end, with y_intermediate
75+
slope_at_t_end_using_y_intermediate = -alpha * y_intermediate + np.sin(omega * t_current_step_end)
76+
y_next = y_prev + t_step * slope_at_t_end_using_y_intermediate
77+
y.append(y_next)
78+
79+
t_points_tensor = torch.tensor(t_points, dtype=torch.float32).view(-1, 1)
80+
y_values_tensor = torch.tensor(y, dtype=torch.float32).view(-1, 1)
81+
82+
return t_points_tensor, y_values_tensor

0 commit comments

Comments
 (0)