Source code for ddmtolab.Problems.RWO.pinn_hpo

import torch
import torch.nn as nn
from tqdm import tqdm
import time
from ddmtolab.Problems.RWO.data_pinn_hpo.pinnhpo_utils import get_data_2d, plot_func_2d
from ddmtolab.Problems.BasicFunctions.basic_functions import *
from ddmtolab.Methods.mtop import MTOP
import warnings
warnings.filterwarnings('ignore', category=UserWarning)


class Sin(nn.Module):
    """
    Custom Sin activation function.

    Implements $f(x) = \sin(x)$ as a PyTorch module.
    """

    def forward(self, x):
        return torch.sin(x)


class Swish(nn.Module):
    """
    Custom Swish activation function.

    Implements $f(x) = x \cdot \sigma(x)$ where $\sigma$ is the sigmoid function.
    """

    def forward(self, x):
        return x * torch.sigmoid(x)


class PINNs(nn.Module):
    """
    Physics-Informed Neural Network (PINN).

    A fully-connected neural network designed for solving partial differential
    equations (PDEs) using physics-informed constraints.

    Parameters
    ----------
    in_dim : int
        Input dimension (number of spatial/temporal coordinates).
    hidden_dim : int
        Number of nodes in each hidden layer.
    out_dim : int
        Output dimension (number of solution components).
    num_layer : int
        Total number of layers including output layer.
        For example, num_layer=3 means 2 hidden layers + 1 output layer.
    activation : str or float, optional
        Activation function type (default is 'tanh').
        - String options: 'tanh', 'relu', 'sigmoid', 'sin', 'swish'
        - Numerical mapping: [0, 1) -> tanh, [1, 2) -> relu, [2, 3) -> sigmoid,
          [3, 4) -> sin, [4, 5] -> swish

    Attributes
    ----------
    network : nn.Sequential
        The sequential neural network model.
    """

    def __init__(self, in_dim, hidden_dim, out_dim, num_layer, activation='tanh'):
        super(PINNs, self).__init__()

        # Activation function mapping for string input
        activation_map = {
            'tanh': nn.Tanh,
            'relu': nn.ReLU,
            'sigmoid': nn.Sigmoid,
            'sin': Sin,
            'swish': Swish
        }

        # Activation function mapping for numerical input
        activation_num_map = {
            0: nn.Tanh,  # [0, 1) -> tanh
            1: nn.ReLU,  # [1, 2) -> relu
            2: nn.Sigmoid,  # [2, 3) -> sigmoid
            3: Sin,  # [3, 4) -> sin
            4: Swish  # [4, 5] -> swish
        }

        # Determine activation function class
        if isinstance(activation, (int, float)):
            # Numerical input: validate range first
            if activation < 0 or activation > 5:
                raise ValueError(f"Numerical activation {activation} out of range. "
                                 f"Expected [0, 5], maps to: 0=tanh, 1=relu, 2=sigmoid, 3=sin, 4=swish")

            # Convert to integer index, handle special case for 5
            activation_idx = int(activation) if activation < 5 else 4
            activation_class = activation_num_map[activation_idx]
        elif isinstance(activation, str):
            # String input
            if activation.lower() not in activation_map:
                raise ValueError(f"Activation '{activation}' not supported. "
                                 f"Available: {list(activation_map.keys())}")
            activation_class = activation_map[activation.lower()]
        else:
            raise TypeError(f"Activation must be string or number, got {type(activation)}")

        # Build network layers
        layers = []
        for i in range(num_layer - 1):
            # First layer: in_dim -> hidden_dim; Other layers: hidden_dim -> hidden_dim
            in_features = in_dim if i == 0 else hidden_dim
            layers.append(nn.Linear(in_features, hidden_dim))
            layers.append(activation_class())

        # Output layer (no activation)
        layers.append(nn.Linear(hidden_dim, out_dim))

        self.network = nn.Sequential(*layers)

    def forward(self, x):
        return self.network(x)


def convection_2d(num_layers, num_nodes, activation_func, epochs, grid_size, learning_rate, test_name, beta=30,
                  plotshow=False, show_information=True, device='cuda:0'):
    """
    Solves the 2D **Convection equation** using PINN.

    The PDE is: $\frac{\partial u}{\partial t} + \beta \frac{\partial u}{\partial x} = 0$.
    Analytical solution: $u(x, t) = \sin(x - \beta t)$.

    Parameters
    ----------
    num_layers : int
        Number of layers in the neural network.
    num_nodes : int
        Number of nodes in each hidden layer.
    activation_func : str or float
        Activation function for the network.
    epochs : int
        Number of training epochs.
    grid_size : int
        Grid resolution for training points (grid_size x grid_size).
    learning_rate : float
        Learning rate for the Adam optimizer.
    test_name : str
        Name identifier for the test run.
    beta : float, optional
        Convection velocity parameter (default is 30).
    plotshow : bool, optional
        Whether to plot the results (default is False).
    show_information : bool, optional
        Whether to display training progress (default is True).
    device : str, optional
        Device for computation, e.g., 'cuda:0' (default is 'cuda:0').

    Returns
    -------
    dict
        Dictionary containing training results including final errors, losses, and runtime.
    """

    # PDEs
    def pde(x, y):
        x.requires_grad_(True)
        dy = torch.autograd.grad(y, x, torch.ones_like(y), create_graph=True)[0]
        dy_t = dy[:, 1:2]
        dy_x = dy[:, 0:1]
        return dy_t + beta * dy_x

    # Analytical solution
    def func(x):
        return torch.sin(x[:, 0:1] - beta * x[:, 1:2])

    # Loss function
    def losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0, lam_bc=1.0, lam_ic=1.0):
        points = points.clone().requires_grad_(True)
        pred_points = model(points)
        pde_residual = pde(points, pred_points)
        pde_loss = torch.mean(pde_residual ** 2)

        pred_b1 = model(b1)
        pred_b2 = model(b2)
        bc_loss = torch.mean((pred_b1 - pred_b2) ** 2)

        pred_b3 = model(b3)
        true_b3 = func(b3)
        ic_loss = torch.mean((pred_b3 - true_b3) ** 2)

        pred_test = model(points_test)
        true_test = func(points_test)
        l1_ab_metric = torch.mean(torch.abs(pred_test - true_test))
        l1_re_metric = torch.mean(torch.abs(pred_test - true_test)) / torch.mean(torch.abs(true_test))
        l2_ab_metric = torch.mean((pred_test - true_test) ** 2)
        l2_re_metric = torch.sqrt(torch.mean((pred_test - true_test) ** 2)) / torch.sqrt(torch.mean(true_test ** 2))

        total_loss = lam_pde * pde_loss + lam_bc * bc_loss + lam_ic * ic_loss

        return total_loss, {
            'pde_loss': pde_loss.item(),
            'bc_loss': bc_loss.item(),
            'ic_loss': ic_loss.item(),
            'total_loss': total_loss.item(),
            'l1_ab_metric': l1_ab_metric.item(),
            'l1_re_metric': l1_re_metric.item(),
            'l2_ab_metric': l2_ab_metric.item(),
            'l2_re_metric': l2_re_metric.item()
        }

    # Initialization
    def init_weights(m):
        if isinstance(m, nn.Linear):
            torch.nn.init.xavier_uniform_(m.weight)
            m.bias.data.fill_(0.01)

    try:
        start_time = time.time()

        # Generate training and test points
        num_x = grid_size
        num_y = grid_size
        num_x_test = 100
        num_y_test = 100
        range_x = torch.tensor([[0., 2 * torch.pi], [0., 1.]]).to(device)

        points, b1, b2, b3, b4 = get_data_2d(num_x, num_y, range_x, device)
        points_test, _, _, _, _ = get_data_2d(num_x_test, num_y_test, range_x, device)

        # Create model with specified parameters
        model = PINNs(in_dim=2, hidden_dim=num_nodes, out_dim=1, num_layer=num_layers, activation=activation_func).to(
            device)
        model.apply(init_weights)

        # Setup optimizer
        optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

        # Training loop
        train_losses_history = []

        # Use tqdm only if show_information is True
        if show_information:
            epoch_iterator = tqdm(range(epochs), desc=f"Training {test_name}")
        else:
            epoch_iterator = range(epochs)

        for epoch in epoch_iterator:
            def closure():
                total_loss, train_loss_dict = losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0,
                                                     lam_bc=1.0, lam_ic=1.0)
                optimizer.zero_grad()
                total_loss.backward()
                if not hasattr(closure, 'latest_loss_dict'):
                    closure.latest_loss_dict = train_loss_dict
                else:
                    closure.latest_loss_dict.update(train_loss_dict)
                return total_loss

            optimizer.step(closure)

            if hasattr(closure, 'latest_loss_dict'):
                train_losses_history.append(closure.latest_loss_dict.copy())

        end_time = time.time()
        runtime = end_time - start_time

        if plotshow == True:
            plot_func_2d(points_test, model, func, range_x, save_path=None, test_name=test_name)

        # Get final metrics
        final_l1_ab_error = train_losses_history[-1]['l1_ab_metric']
        final_l1_re_error = train_losses_history[-1]['l1_re_metric']
        final_l2_ab_error = train_losses_history[-1]['l2_ab_metric']
        final_l2_re_error = train_losses_history[-1]['l2_re_metric']
        final_pde_loss = train_losses_history[-1]['pde_loss']
        final_bc_loss = train_losses_history[-1]['bc_loss']
        final_ic_loss = train_losses_history[-1]['ic_loss']
        final_total_loss = train_losses_history[-1]['total_loss']

        return {
            'test_name': test_name,
            'final_l1_ab_error': final_l1_ab_error,
            'final_l1_re_error': final_l1_re_error,
            'final_l2_ab_error': final_l2_ab_error,
            'final_l2_re_error': final_l2_re_error,
            'final_pde_loss': final_pde_loss,
            'final_bc_loss': final_bc_loss,
            'final_ic_loss': final_ic_loss,
            'final_total_loss': final_total_loss,
            'runtime': runtime,
            'num_layers': num_layers,
            'num_nodes': num_nodes,
            'activation_func': activation_func,
            'epochs': epochs,
            'grid_size': grid_size,
            'learning_rate': learning_rate
        }

    except Exception as e:
        if show_information:
            print("\n" + "=" * 80)
            print(f"ERROR in {test_name}: {str(e)}")
            print("=" * 80 + "\n")
            import traceback
            traceback.print_exc()

        return {
            'test_name': test_name,
            'final_l2_error': float('inf'),
            'runtime': 0,
            'error': str(e)
        }


def reaction_2d(num_layers, num_nodes, activation_func, epochs, grid_size, learning_rate, test_name, rho=4,
                plotshow=False, show_information=True, device='cuda:0'):
    """
    Solves the 2D **Reaction equation** using PINN.

    The PDE is: $\frac{\partial u}{\partial t} = \rho u (1 - u)$.
    Analytical solution involves a Gaussian initial condition evolving over time.

    Parameters
    ----------
    num_layers : int
        Number of layers in the neural network.
    num_nodes : int
        Number of nodes in each hidden layer.
    activation_func : str or float
        Activation function for the network.
    epochs : int
        Number of training epochs.
    grid_size : int
        Grid resolution for training points (grid_size x grid_size).
    learning_rate : float
        Learning rate for the Adam optimizer.
    test_name : str
        Name identifier for the test run.
    rho : float, optional
        Reaction rate parameter (default is 4).
    plotshow : bool, optional
        Whether to plot the results (default is False).
    show_information : bool, optional
        Whether to display training progress (default is True).
    device : str, optional
        Device for computation, e.g., 'cuda:0' (default is 'cuda:0').

    Returns
    -------
    dict
        Dictionary containing training results including final errors, losses, and runtime.
    """

    # PDEs
    def pde(x, y):
        x.requires_grad_(True)
        dy = torch.autograd.grad(y, x, torch.ones_like(y), create_graph=True)[0]
        dy_t = dy[:, 1:2]
        return dy_t - rho * y * (1 - y)

    # Analytical solution
    def func(x):
        h = torch.exp(-(x[:, 0:1] - torch.pi) ** 2 / (2 * (torch.pi / 4) ** 2))
        return h * torch.exp(rho * x[:, 1:2]) / (h * torch.exp(rho * x[:, 1:2]) + 1 - h)

    # Loss function
    def losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0, lam_bc=1.0, lam_ic=1.0):
        points = points.clone().requires_grad_(True)
        pred_points = model(points)
        pde_residual = pde(points, pred_points)
        pde_loss = torch.mean(pde_residual ** 2)

        pred_b1 = model(b1)
        pred_b2 = model(b2)
        bc_loss = torch.mean((pred_b1 - pred_b2) ** 2)

        pred_b3 = model(b3)
        true_b3 = torch.exp(-(b3[:, 0:1] - torch.pi) ** 2 / (2 * (torch.pi / 4) ** 2))
        ic_loss = torch.mean((pred_b3 - true_b3) ** 2)

        pred_test = model(points_test)
        true_test = func(points_test)
        l1_ab_metric = torch.mean(torch.abs(pred_test - true_test))
        l1_re_metric = torch.mean(torch.abs(pred_test - true_test)) / torch.mean(torch.abs(true_test))
        l2_ab_metric = torch.mean((pred_test - true_test) ** 2)
        l2_re_metric = torch.sqrt(torch.mean((pred_test - true_test) ** 2)) / torch.sqrt(torch.mean(true_test ** 2))

        total_loss = lam_pde * pde_loss + lam_bc * bc_loss + lam_ic * ic_loss

        return total_loss, {
            'pde_loss': pde_loss.item(),
            'bc_loss': bc_loss.item(),
            'ic_loss': ic_loss.item(),
            'total_loss': total_loss.item(),
            'l1_ab_metric': l1_ab_metric.item(),
            'l1_re_metric': l1_re_metric.item(),
            'l2_ab_metric': l2_ab_metric.item(),
            'l2_re_metric': l2_re_metric.item()
        }

    # Initialization
    def init_weights(m):
        if isinstance(m, nn.Linear):
            torch.nn.init.xavier_uniform_(m.weight)
            m.bias.data.fill_(0.01)

    try:
        start_time = time.time()

        # Generate training and test points
        num_x = grid_size
        num_y = grid_size
        num_x_test = 100
        num_y_test = 100
        range_x = torch.tensor([[0., 2 * torch.pi], [0., 1.]]).to(device)

        points, b1, b2, b3, b4 = get_data_2d(num_x, num_y, range_x, device)
        points_test, _, _, _, _ = get_data_2d(num_x_test, num_y_test, range_x, device)

        # Create model with specified parameters
        model = PINNs(in_dim=2, hidden_dim=num_nodes, out_dim=1, num_layer=num_layers, activation=activation_func).to(
            device)
        model.apply(init_weights)

        # Setup optimizer
        optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

        # Training loop
        train_losses_history = []

        # Use tqdm only if show_information is True
        if show_information:
            epoch_iterator = tqdm(range(epochs), desc=f"Training {test_name}")
        else:
            epoch_iterator = range(epochs)

        for epoch in epoch_iterator:
            def closure():
                total_loss, train_loss_dict = losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0,
                                                     lam_bc=1.0, lam_ic=1.0)
                optimizer.zero_grad()
                total_loss.backward()
                if not hasattr(closure, 'latest_loss_dict'):
                    closure.latest_loss_dict = train_loss_dict
                else:
                    closure.latest_loss_dict.update(train_loss_dict)
                return total_loss

            optimizer.step(closure)

            if hasattr(closure, 'latest_loss_dict'):
                train_losses_history.append(closure.latest_loss_dict.copy())

        end_time = time.time()
        runtime = end_time - start_time

        if plotshow == True:
            plot_func_2d(points_test, model, func, range_x, save_path=None, test_name=test_name)

        # Get final metrics
        final_l1_ab_error = train_losses_history[-1]['l1_ab_metric']
        final_l1_re_error = train_losses_history[-1]['l1_re_metric']
        final_l2_ab_error = train_losses_history[-1]['l2_ab_metric']
        final_l2_re_error = train_losses_history[-1]['l2_re_metric']
        final_pde_loss = train_losses_history[-1]['pde_loss']
        final_bc_loss = train_losses_history[-1]['bc_loss']
        final_ic_loss = train_losses_history[-1]['ic_loss']
        final_total_loss = train_losses_history[-1]['total_loss']

        return {
            'test_name': test_name,
            'final_l1_ab_error': final_l1_ab_error,
            'final_l1_re_error': final_l1_re_error,
            'final_l2_ab_error': final_l2_ab_error,
            'final_l2_re_error': final_l2_re_error,
            'final_pde_loss': final_pde_loss,
            'final_bc_loss': final_bc_loss,
            'final_ic_loss': final_ic_loss,
            'final_total_loss': final_total_loss,
            'runtime': runtime,
            'num_layers': num_layers,
            'num_nodes': num_nodes,
            'activation_func': activation_func,
            'epochs': epochs,
            'grid_size': grid_size,
            'learning_rate': learning_rate
        }

    except Exception as e:
        if show_information:
            print("\n" + "=" * 80)
            print(f"ERROR in {test_name}: {str(e)}")
            print("=" * 80 + "\n")
            import traceback
            traceback.print_exc()

        return {
            'test_name': test_name,
            'final_l2_error': float('inf'),
            'runtime': 0,
            'error': str(e)
        }


def wave_2d(num_layers, num_nodes, activation_func, epochs, grid_size, learning_rate, test_name, alpha=4, beta=3,
            plotshow=False, show_information=True, device='cuda:0'):
    """
    Solves the 2D **Wave equation** using PINN.

    The PDE is: $\frac{\partial^2 u}{\partial t^2} = \alpha \frac{\partial^2 u}{\partial x^2}$.
    Analytical solution: $u(x, t) = \sin(\pi x) \cos(\sqrt{\alpha} \pi t) + 0.5 \sin(\beta \pi x) \cos(\sqrt{\alpha} \beta \pi t)$.

    Parameters
    ----------
    num_layers : int
        Number of layers in the neural network.
    num_nodes : int
        Number of nodes in each hidden layer.
    activation_func : str or float
        Activation function for the network.
    epochs : int
        Number of training epochs.
    grid_size : int
        Grid resolution for training points (grid_size x grid_size).
    learning_rate : float
        Learning rate for the Adam optimizer.
    test_name : str
        Name identifier for the test run.
    alpha : float, optional
        Wave speed squared parameter (default is 4).
    beta : float, optional
        Frequency multiplier for the second mode (default is 3).
    plotshow : bool, optional
        Whether to plot the results (default is False).
    show_information : bool, optional
        Whether to display training progress (default is True).
    device : str, optional
        Device for computation, e.g., 'cuda:0' (default is 'cuda:0').

    Returns
    -------
    dict
        Dictionary containing training results including final errors, losses, and runtime.
    """

    # PDEs
    def pde(x, u):
        x.requires_grad_(True)
        du = torch.autograd.grad(u, x, torch.ones_like(u), create_graph=True, retain_graph=True)[0]
        du_t = du[:, 1:2]
        du_x = du[:, 0:1]
        du_tt = torch.autograd.grad(du_t, x, torch.ones_like(du_t), create_graph=True)[0][:, 1:2]
        du_xx = torch.autograd.grad(du_x, x, torch.ones_like(du_x), create_graph=True)[0][:, 0:1]
        return du_tt - alpha * du_xx

    def ic(x, u):
        x.requires_grad_(True)
        du = torch.autograd.grad(u, x, torch.ones_like(u), create_graph=True, retain_graph=True)[0]
        ic1 = du[:, 1:2]
        ic2 = torch.sin(torch.pi * x[:, 0:1]) + 0.5 * torch.sin(beta * torch.pi * x[:, 0:1]) - u
        return ic1, ic2

    # Analytical solution
    def func(x):
        x_coord = x[:, 0:1]
        t_coord = x[:, 1:2]
        pi = torch.tensor(torch.pi)
        sqrt_alpha = torch.sqrt(torch.tensor(alpha))
        term1 = torch.sin(pi * x_coord) * torch.cos(sqrt_alpha * pi * t_coord)
        term2 = 0.5 * torch.sin(beta * pi * x_coord) * torch.cos(sqrt_alpha * beta * pi * t_coord)
        return term1 + term2

    # Loss function
    def losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0, lam_bc=1.0, lam_ic=1.0):
        points = points.clone().requires_grad_(True)
        pred_points = model(points)
        pde_residual = pde(points, pred_points)
        pde_loss = torch.mean(pde_residual ** 2)

        pred_b1 = model(b1)
        pred_b2 = model(b2)
        bc_loss = torch.mean((pred_b1) ** 2) + torch.mean((pred_b2) ** 2)

        b3 = b3.clone().requires_grad_(True)
        pred_b3 = model(b3)
        ic_res_1, ic_res_2 = ic(b3, pred_b3)
        ic_loss = torch.mean((ic_res_1) ** 2) + torch.mean((ic_res_2) ** 2)

        pred_test = model(points_test)
        true_test = func(points_test)
        l1_ab_metric = torch.mean(torch.abs(pred_test - true_test))
        l1_re_metric = torch.mean(torch.abs(pred_test - true_test)) / torch.mean(torch.abs(true_test))
        l2_ab_metric = torch.mean((pred_test - true_test) ** 2)
        l2_re_metric = torch.sqrt(torch.mean((pred_test - true_test) ** 2)) / torch.sqrt(torch.mean(true_test ** 2))

        total_loss = lam_pde * pde_loss + lam_bc * bc_loss + lam_ic * ic_loss

        return total_loss, {
            'pde_loss': pde_loss.item(),
            'bc_loss': bc_loss.item(),
            'ic_loss': ic_loss.item(),
            'total_loss': total_loss.item(),
            'l1_ab_metric': l1_ab_metric.item(),
            'l1_re_metric': l1_re_metric.item(),
            'l2_ab_metric': l2_ab_metric.item(),
            'l2_re_metric': l2_re_metric.item()
        }

    # Initialization
    def init_weights(m):
        if isinstance(m, nn.Linear):
            torch.nn.init.xavier_uniform_(m.weight)
            m.bias.data.fill_(0.01)

    try:
        start_time = time.time()

        # Generate training and test points
        num_x = grid_size
        num_y = grid_size
        num_x_test = 100
        num_y_test = 100
        range_x = torch.tensor([[0., 1.], [0., 1.]]).to(device)

        points, b1, b2, b3, b4 = get_data_2d(num_x, num_y, range_x, device)
        points_test, _, _, _, _ = get_data_2d(num_x_test, num_y_test, range_x, device)

        # Create model with specified parameters
        model = PINNs(in_dim=2, hidden_dim=num_nodes, out_dim=1, num_layer=num_layers, activation=activation_func).to(
            device)
        model.apply(init_weights)

        # Setup optimizer
        optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

        # Training loop
        train_losses_history = []

        # Use tqdm only if show_information is True
        if show_information:
            epoch_iterator = tqdm(range(epochs), desc=f"Training {test_name}")
        else:
            epoch_iterator = range(epochs)

        for epoch in epoch_iterator:
            def closure():
                total_loss, train_loss_dict = losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0,
                                                     lam_bc=1.0, lam_ic=1.0)
                optimizer.zero_grad()
                total_loss.backward()
                if not hasattr(closure, 'latest_loss_dict'):
                    closure.latest_loss_dict = train_loss_dict
                else:
                    closure.latest_loss_dict.update(train_loss_dict)
                return total_loss

            optimizer.step(closure)

            if hasattr(closure, 'latest_loss_dict'):
                train_losses_history.append(closure.latest_loss_dict.copy())

        end_time = time.time()
        runtime = end_time - start_time

        if plotshow == True:
            plot_func_2d(points_test, model, func, range_x, save_path=None, test_name=test_name)

        # Get final metrics
        final_l1_ab_error = train_losses_history[-1]['l1_ab_metric']
        final_l1_re_error = train_losses_history[-1]['l1_re_metric']
        final_l2_ab_error = train_losses_history[-1]['l2_ab_metric']
        final_l2_re_error = train_losses_history[-1]['l2_re_metric']
        final_pde_loss = train_losses_history[-1]['pde_loss']
        final_bc_loss = train_losses_history[-1]['bc_loss']
        final_ic_loss = train_losses_history[-1]['ic_loss']
        final_total_loss = train_losses_history[-1]['total_loss']

        return {
            'test_name': test_name,
            'final_l1_ab_error': final_l1_ab_error,
            'final_l1_re_error': final_l1_re_error,
            'final_l2_ab_error': final_l2_ab_error,
            'final_l2_re_error': final_l2_re_error,
            'final_pde_loss': final_pde_loss,
            'final_bc_loss': final_bc_loss,
            'final_ic_loss': final_ic_loss,
            'final_total_loss': final_total_loss,
            'runtime': runtime,
            'num_layers': num_layers,
            'num_nodes': num_nodes,
            'activation_func': activation_func,
            'epochs': epochs,
            'grid_size': grid_size,
            'learning_rate': learning_rate
        }

    except Exception as e:
        if show_information:
            print("\n" + "=" * 80)
            print(f"ERROR in {test_name}: {str(e)}")
            print("=" * 80 + "\n")
            import traceback
            traceback.print_exc()

        return {
            'test_name': test_name,
            'final_l2_error': float('inf'),
            'runtime': 0,
            'error': str(e)
        }


def helmholtz_2d(num_layers, num_nodes, activation_func, epochs, grid_size, learning_rate, test_name, n=2,
                 plotshow=False, show_information=True, device='cuda:0'):
    """
    Solves the 2D **Helmholtz equation** using PINN.

    The PDE is: $-\nabla^2 u - k^2 u = f(x, y)$ where $k = n\pi$ and $f(x, y) = k^2 \sin(kx) \sin(ky)$.
    Analytical solution: $u(x, y) = \sin(kx) \sin(ky)$.

    Parameters
    ----------
    num_layers : int
        Number of layers in the neural network.
    num_nodes : int
        Number of nodes in each hidden layer.
    activation_func : str or float
        Activation function for the network.
    epochs : int
        Number of training epochs.
    grid_size : int
        Grid resolution for training points (grid_size x grid_size).
    learning_rate : float
        Learning rate for the Adam optimizer.
    test_name : str
        Name identifier for the test run.
    n : int, optional
        Wave number multiplier (default is 2). Controls frequency via $k = n\pi$.
    plotshow : bool, optional
        Whether to plot the results (default is False).
    show_information : bool, optional
        Whether to display training progress (default is True).
    device : str, optional
        Device for computation, e.g., 'cuda:0' (default is 'cuda:0').

    Returns
    -------
    dict
        Dictionary containing training results including final errors, losses, and runtime.
    """

    k = n * torch.pi

    # PDEs
    def pde(x, y):
        x.requires_grad_(True)
        dy = torch.autograd.grad(y, x, torch.ones_like(y), create_graph=True)[0]
        dy_x = dy[:, 0:1]
        dy_y = dy[:, 1:2]
        dy_xx = torch.autograd.grad(dy_x, x, torch.ones_like(dy_x), create_graph=True)[0][:, 0:1]
        dy_yy = torch.autograd.grad(dy_y, x, torch.ones_like(dy_y), create_graph=True)[0][:, 1:2]
        f = k ** 2 * torch.sin(k * x[:, 0:1]) * torch.sin(k * x[:, 1:2])
        return -dy_xx - dy_yy - k ** 2 * y - f

    # Analytical solution
    def func(x):
        return torch.sin(k * x[:, 0:1]) * torch.sin(k * x[:, 1:2])

    # Loss function
    def losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0, lam_bc=1.0, lam_ic=1.0):
        points = points.clone().requires_grad_(True)
        pred_points = model(points)
        pde_residual = pde(points, pred_points)
        pde_loss = torch.mean(pde_residual ** 2)

        pred_b1 = model(b1)
        pred_b2 = model(b2)
        pred_b3 = model(b3)
        pred_b4 = model(b4)
        bc_loss = torch.mean((pred_b1) ** 2) + torch.mean((pred_b2) ** 2) + torch.mean((pred_b3) ** 2) + torch.mean(
            (pred_b4) ** 2)

        ic_loss = torch.tensor(0.0, device=device)

        pred_test = model(points_test)
        true_test = func(points_test)
        l1_ab_metric = torch.mean(torch.abs(pred_test - true_test))
        l1_re_metric = torch.mean(torch.abs(pred_test - true_test)) / torch.mean(torch.abs(true_test))
        l2_ab_metric = torch.mean((pred_test - true_test) ** 2)
        l2_re_metric = torch.sqrt(torch.mean((pred_test - true_test) ** 2)) / torch.sqrt(torch.mean(true_test ** 2))

        total_loss = lam_pde * pde_loss + lam_bc * bc_loss + lam_ic * ic_loss

        return total_loss, {
            'pde_loss': pde_loss.item(),
            'bc_loss': bc_loss.item(),
            'ic_loss': ic_loss.item(),
            'total_loss': total_loss.item(),
            'l1_ab_metric': l1_ab_metric.item(),
            'l1_re_metric': l1_re_metric.item(),
            'l2_ab_metric': l2_ab_metric.item(),
            'l2_re_metric': l2_re_metric.item()
        }

    # Initialization
    def init_weights(m):
        if isinstance(m, nn.Linear):
            torch.nn.init.xavier_uniform_(m.weight)
            m.bias.data.fill_(0.01)

    try:
        start_time = time.time()

        # Generate training and test points
        num_x = grid_size
        num_y = grid_size
        num_x_test = 100
        num_y_test = 100
        range_x = torch.tensor([[0., 1.], [0., 1.]]).to(device)

        points, b1, b2, b3, b4 = get_data_2d(num_x, num_y, range_x, device)
        points_test, _, _, _, _ = get_data_2d(num_x_test, num_y_test, range_x, device)

        # Create model with specified parameters
        model = PINNs(in_dim=2, hidden_dim=num_nodes, out_dim=1, num_layer=num_layers, activation=activation_func).to(
            device)
        model.apply(init_weights)

        # Setup optimizer
        optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

        # Training loop
        train_losses_history = []

        # Use tqdm only if show_information is True
        if show_information:
            epoch_iterator = tqdm(range(epochs), desc=f"Training {test_name}")
        else:
            epoch_iterator = range(epochs)

        for epoch in epoch_iterator:
            def closure():
                total_loss, train_loss_dict = losses(model, points, b1, b2, b3, b4, points_test, lam_pde=1.0,
                                                     lam_bc=1.0, lam_ic=1.0)
                optimizer.zero_grad()
                total_loss.backward()
                if not hasattr(closure, 'latest_loss_dict'):
                    closure.latest_loss_dict = train_loss_dict
                else:
                    closure.latest_loss_dict.update(train_loss_dict)
                return total_loss

            optimizer.step(closure)

            if hasattr(closure, 'latest_loss_dict'):
                train_losses_history.append(closure.latest_loss_dict.copy())

        end_time = time.time()
        runtime = end_time - start_time

        if plotshow == True:
            plot_func_2d(points_test, model, func, range_x, save_path=None, test_name=test_name)

        # Get final metrics
        final_l1_ab_error = train_losses_history[-1]['l1_ab_metric']
        final_l1_re_error = train_losses_history[-1]['l1_re_metric']
        final_l2_ab_error = train_losses_history[-1]['l2_ab_metric']
        final_l2_re_error = train_losses_history[-1]['l2_re_metric']
        final_pde_loss = train_losses_history[-1]['pde_loss']
        final_bc_loss = train_losses_history[-1]['bc_loss']
        final_ic_loss = train_losses_history[-1]['ic_loss']
        final_total_loss = train_losses_history[-1]['total_loss']

        return {
            'test_name': test_name,
            'final_l1_ab_error': final_l1_ab_error,
            'final_l1_re_error': final_l1_re_error,
            'final_l2_ab_error': final_l2_ab_error,
            'final_l2_re_error': final_l2_re_error,
            'final_pde_loss': final_pde_loss,
            'final_bc_loss': final_bc_loss,
            'final_ic_loss': final_ic_loss,
            'final_total_loss': final_total_loss,
            'runtime': runtime,
            'num_layers': num_layers,
            'num_nodes': num_nodes,
            'activation_func': activation_func,
            'epochs': epochs,
            'grid_size': grid_size,
            'learning_rate': learning_rate
        }

    except Exception as e:
        if show_information:
            print("\n" + "=" * 80)
            print(f"ERROR in {test_name}: {str(e)}")
            print("=" * 80 + "\n")
            import traceback
            traceback.print_exc()

        return {
            'test_name': test_name,
            'final_l2_error': float('inf'),
            'runtime': 0,
            'error': str(e)
        }


[docs] class PINN_HPO: """ Physics-Informed Neural Network Hyperparameter Optimization (PINN-HPO) benchmark suite. This class provides a collection of multi-task optimization problems for tuning PINN hyperparameters across different PDEs (Convection, Reaction, Wave, Helmholtz). Each problem consists of multiple related tasks with varying PDE parameters. Notes ----- Decision variables for all problems: - x[0]: Number of layers (integer, [2, 10]) - x[1]: Number of nodes per layer (integer, [5, 100]) - x[2]: Activation function (float, [0, 5] mapping to tanh/relu/sigmoid/sin/swish) - x[3]: Training epochs (integer, [5000, 100000]) - x[4]: Grid size (integer, [10, 200]) - x[5]: Learning rate (float, [1e-5, 0.1]) """ problem_information = { 'n_cases': 12, 'n_tasks': '[2, 4]', 'n_dims': '6', 'n_objs': '1', 'n_cons': '0', 'type': 'real_world', } @staticmethod def _evaluate_convection(x, beta, test_name, device): """ Evaluates PINN hyperparameters on the Convection equation task. Parameters ---------- x : np.ndarray Hyperparameter configuration array. beta : float Convection velocity parameter. test_name : str Task identifier. device : str CUDA device for computation. Returns ------- np.ndarray L2 relative error for each sample. """ if x.ndim == 1: x = x.reshape(1, -1) n_samples = x.shape[0] results = [] for i in range(n_samples): sample = x[i] n_layers = int(round(sample[0])) n_nodes = int(round(sample[1])) activation = float(sample[2]) epochs = int(round(sample[3])) grid_size = int(round(sample[4])) learning_rate = float(sample[5]) result = convection_2d( num_layers=n_layers, num_nodes=n_nodes, activation_func=activation, epochs=epochs, grid_size=grid_size, learning_rate=learning_rate, test_name=test_name, beta=beta, show_information=False, device=device ) error = result.get('final_l2_re_error', float('inf')) error = min(error, 3.0) results.append(error) return np.array(results).reshape(-1, 1) @staticmethod def _evaluate_reaction(x, rho, test_name, device): """ Evaluates PINN hyperparameters on the Reaction equation task. Parameters ---------- x : np.ndarray Hyperparameter configuration array. rho : float Reaction rate parameter. test_name : str Task identifier. device : str CUDA device for computation. Returns ------- np.ndarray L2 relative error for each sample. """ if x.ndim == 1: x = x.reshape(1, -1) n_samples = x.shape[0] results = [] for i in range(n_samples): sample = x[i] n_layers = int(round(sample[0])) n_nodes = int(round(sample[1])) activation = float(sample[2]) epochs = int(round(sample[3])) grid_size = int(round(sample[4])) learning_rate = float(sample[5]) result = reaction_2d( num_layers=n_layers, num_nodes=n_nodes, activation_func=activation, epochs=epochs, grid_size=grid_size, learning_rate=learning_rate, test_name=test_name, rho=rho, show_information=False, device=device ) error = result.get('final_l2_re_error', float('inf')) error = min(error, 3.0) results.append(error) return np.array(results).reshape(-1, 1) @staticmethod def _evaluate_wave(x, alpha, beta, test_name, device): """ Evaluates PINN hyperparameters on the Wave equation task. Parameters ---------- x : np.ndarray Hyperparameter configuration array. alpha : float Wave speed squared parameter. beta : float Frequency multiplier for the second mode. test_name : str Task identifier. device : str CUDA device for computation. Returns ------- np.ndarray L2 relative error for each sample. """ if x.ndim == 1: x = x.reshape(1, -1) n_samples = x.shape[0] results = [] for i in range(n_samples): sample = x[i] n_layers = int(round(sample[0])) n_nodes = int(round(sample[1])) activation = float(sample[2]) epochs = int(round(sample[3])) grid_size = int(round(sample[4])) learning_rate = float(sample[5]) result = wave_2d( num_layers=n_layers, num_nodes=n_nodes, activation_func=activation, epochs=epochs, grid_size=grid_size, learning_rate=learning_rate, test_name=test_name, alpha=alpha, beta=beta, show_information=False, device=device ) error = result.get('final_l2_re_error', float('inf')) error = min(error, 3.0) results.append(error) return np.array(results).reshape(-1, 1) @staticmethod def _evaluate_helmholtz(x, n, test_name, device): """ Evaluates PINN hyperparameters on the Helmholtz equation task. Parameters ---------- x : np.ndarray Hyperparameter configuration array. n : int Wave number multiplier (controls frequency). test_name : str Task identifier. device : str CUDA device for computation. Returns ------- np.ndarray L2 relative error for each sample. """ if x.ndim == 1: x = x.reshape(1, -1) n_samples = x.shape[0] results = [] for i in range(n_samples): sample = x[i] n_layers = int(round(sample[0])) n_nodes = int(round(sample[1])) activation = float(sample[2]) epochs = int(round(sample[3])) grid_size = int(round(sample[4])) learning_rate = float(sample[5]) result = helmholtz_2d( num_layers=n_layers, num_nodes=n_nodes, activation_func=activation, epochs=epochs, grid_size=grid_size, learning_rate=learning_rate, test_name=test_name, n=n, show_information=False, device=device ) error = result.get('final_l2_re_error', float('inf')) error = min(error, 3.0) results.append(error) return np.array(results).reshape(-1, 1) # Problem 1: Convection (β=20, β=30)
[docs] @staticmethod def T1_convection_beta20(x): return PINN_HPO._evaluate_convection(x, beta=20, test_name='T1_convection_beta20', device='cuda:0')
[docs] @staticmethod def T1_convection_beta30(x): return PINN_HPO._evaluate_convection(x, beta=30, test_name='T1_convection_beta30', device='cuda:1')
# Problem 2: Reaction (ρ=4, ρ=5)
[docs] @staticmethod def T2_reaction_rho4(x): return PINN_HPO._evaluate_reaction(x, rho=4, test_name='T2_reaction_rho4', device='cuda:0')
[docs] @staticmethod def T2_reaction_rho5(x): return PINN_HPO._evaluate_reaction(x, rho=5, test_name='T2_reaction_rho5', device='cuda:1')
# Problem 3: Wave (α=3,β=3; α=4,β=3)
[docs] @staticmethod def T3_wave_alpha3_beta3(x): return PINN_HPO._evaluate_wave(x, alpha=3, beta=3, test_name='T3_wave_alpha3_beta3', device='cuda:0')
[docs] @staticmethod def T3_wave_alpha4_beta3(x): return PINN_HPO._evaluate_wave(x, alpha=4, beta=3, test_name='T3_wave_alpha4_beta3', device='cuda:1')
# Problem 4: Helmholtz (n=3, n=4)
[docs] @staticmethod def T4_helmholtz_n3(x): return PINN_HPO._evaluate_helmholtz(x, n=3, test_name='T4_helmholtz_n3', device='cuda:0')
[docs] @staticmethod def T4_helmholtz_n4(x): return PINN_HPO._evaluate_helmholtz(x, n=4, test_name='T4_helmholtz_n4', device='cuda:1')
# Problem 5: Convection (β=20, β=30, β=40)
[docs] @staticmethod def T5_convection_beta20(x): return PINN_HPO._evaluate_convection(x, beta=20, test_name='T5_convection_beta20', device='cuda:0')
[docs] @staticmethod def T5_convection_beta30(x): return PINN_HPO._evaluate_convection(x, beta=30, test_name='T5_convection_beta30', device='cuda:1')
[docs] @staticmethod def T5_convection_beta40(x): return PINN_HPO._evaluate_convection(x, beta=40, test_name='T5_convection_beta40', device='cuda:2')
# Problem 6: Reaction (ρ=4, ρ=5, ρ=6)
[docs] @staticmethod def T6_reaction_rho4(x): return PINN_HPO._evaluate_reaction(x, rho=4, test_name='T6_reaction_rho4', device='cuda:0')
[docs] @staticmethod def T6_reaction_rho5(x): return PINN_HPO._evaluate_reaction(x, rho=5, test_name='T6_reaction_rho5', device='cuda:1')
[docs] @staticmethod def T6_reaction_rho6(x): return PINN_HPO._evaluate_reaction(x, rho=6, test_name='T6_reaction_rho6', device='cuda:2')
# Problem 7: Wave (α=3,β=3; α=4,β=3; α=4,β=4)
[docs] @staticmethod def T7_wave_alpha3_beta3(x): return PINN_HPO._evaluate_wave(x, alpha=3, beta=3, test_name='T7_wave_alpha3_beta3', device='cuda:0')
[docs] @staticmethod def T7_wave_alpha4_beta3(x): return PINN_HPO._evaluate_wave(x, alpha=4, beta=3, test_name='T7_wave_alpha4_beta3', device='cuda:1')
[docs] @staticmethod def T7_wave_alpha4_beta4(x): return PINN_HPO._evaluate_wave(x, alpha=4, beta=4, test_name='T7_wave_alpha4_beta4', device='cuda:2')
# Problem 8: Helmholtz (n=3, n=4, n=5)
[docs] @staticmethod def T8_helmholtz_n3(x): return PINN_HPO._evaluate_helmholtz(x, n=3, test_name='T8_helmholtz_n3', device='cuda:0')
[docs] @staticmethod def T8_helmholtz_n4(x): return PINN_HPO._evaluate_helmholtz(x, n=4, test_name='T8_helmholtz_n4', device='cuda:1')
[docs] @staticmethod def T8_helmholtz_n5(x): return PINN_HPO._evaluate_helmholtz(x, n=5, test_name='T8_helmholtz_n5', device='cuda:2')
# Problem 9: Mixed (Convection β=30, Reaction ρ=5)
[docs] @staticmethod def T9_convection_beta30(x): return PINN_HPO._evaluate_convection(x, beta=30, test_name='T9_convection_beta30', device='cuda:0')
[docs] @staticmethod def T9_reaction_rho5(x): return PINN_HPO._evaluate_reaction(x, rho=5, test_name='T9_reaction_rho5', device='cuda:1')
# Problem 10: Mixed (Wave α=4,β=3; Helmholtz n=4)
[docs] @staticmethod def T10_wave_alpha4_beta3(x): return PINN_HPO._evaluate_wave(x, alpha=4, beta=3, test_name='T10_wave_alpha4_beta3', device='cuda:0')
[docs] @staticmethod def T10_helmholtz_n4(x): return PINN_HPO._evaluate_helmholtz(x, n=4, test_name='T10_helmholtz_n4', device='cuda:1')
# Problem 11: Mixed (Convection β=30, Reaction ρ=5, Wave α=4,β=3)
[docs] @staticmethod def T11_convection_beta30(x): return PINN_HPO._evaluate_convection(x, beta=30, test_name='T11_convection_beta30', device='cuda:0')
[docs] @staticmethod def T11_reaction_rho5(x): return PINN_HPO._evaluate_reaction(x, rho=5, test_name='T11_reaction_rho5', device='cuda:1')
[docs] @staticmethod def T11_wave_alpha4_beta3(x): return PINN_HPO._evaluate_wave(x, alpha=4, beta=3, test_name='T11_wave_alpha4_beta3', device='cuda:2')
# Problem 12: Mixed (Convection β=30, Reaction ρ=5, Wave α=4,β=3, Helmholtz n=4)
[docs] @staticmethod def T12_convection_beta30(x): return PINN_HPO._evaluate_convection(x, beta=30, test_name='T12_convection_beta30', device='cuda:0')
[docs] @staticmethod def T12_reaction_rho5(x): return PINN_HPO._evaluate_reaction(x, rho=5, test_name='T12_reaction_rho5', device='cuda:1')
[docs] @staticmethod def T12_wave_alpha4_beta3(x): return PINN_HPO._evaluate_wave(x, alpha=4, beta=3, test_name='T12_wave_alpha4_beta3', device='cuda:2')
[docs] @staticmethod def T12_helmholtz_n4(x): return PINN_HPO._evaluate_helmholtz(x, n=4, test_name='T12_helmholtz_n4', device='cuda:3')
[docs] def P1(self): """ Generates Problem 1: **Convection** (:math:`\\beta=20`, :math:`\\beta=30`). Two-task hyperparameter optimization for Convection PDE with different convection velocities. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T1_convection_beta20, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T1_convection_beta30, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P2(self): """ Generates Problem 2: **Reaction** (:math:`\\rho=4`, :math:`\\rho=5`). Two-task hyperparameter optimization for Reaction PDE with different reaction rates. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T2_reaction_rho4, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T2_reaction_rho5, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P3(self): """ Generates Problem 3: **Wave** (:math:`\\alpha=3, \\beta=3`; :math:`\\alpha=4, \\beta=3`). Two-task hyperparameter optimization for Wave PDE with different wave speed parameters. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T3_wave_alpha3_beta3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T3_wave_alpha4_beta3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P4(self): """ Generates Problem 4: **Helmholtz** (:math:`n=3`, :math:`n=4`). Two-task hyperparameter optimization for Helmholtz PDE with different wave number multipliers. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T4_helmholtz_n3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T4_helmholtz_n4, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P5(self): """ Generates Problem 5: **Convection** (:math:`\\beta=20`, :math:`\\beta=30`, :math:`\\beta=40`). Three-task hyperparameter optimization for Convection PDE with different convection velocities. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T5_convection_beta20, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T5_convection_beta30, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T5_convection_beta40, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P6(self): """ Generates Problem 6: **Reaction** (:math:`\\rho=4`, :math:`\\rho=5`, :math:`\\rho=6`). Three-task hyperparameter optimization for Reaction PDE with different reaction rates. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T6_reaction_rho4, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T6_reaction_rho5, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T6_reaction_rho6, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P7(self): """ Generates Problem 7: **Wave** (:math:`\\alpha=3, \\beta=3`; :math:`\\alpha=4, \\beta=3`; :math:`\\alpha=4, \\beta=4`). Three-task hyperparameter optimization for Wave PDE with different wave speed and frequency parameters. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T7_wave_alpha3_beta3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T7_wave_alpha4_beta3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T7_wave_alpha4_beta4, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P8(self): """ Generates Problem 8: **Helmholtz** (:math:`n=3`, :math:`n=4`, :math:`n=5`). Three-task hyperparameter optimization for Helmholtz PDE with different wave number multipliers. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T8_helmholtz_n3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T8_helmholtz_n4, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T8_helmholtz_n5, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P9(self): """ Generates Problem 9: **Mixed** (Convection :math:`\\beta=30`, Reaction :math:`\\rho=5`). Two-task mixed hyperparameter optimization combining Convection and Reaction PDEs. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T9_convection_beta30, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T9_reaction_rho5, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P10(self): """ Generates Problem 10: **Mixed** (Wave :math:`\\alpha=4, \\beta=3`; Helmholtz :math:`n=4`). Two-task mixed hyperparameter optimization combining Wave and Helmholtz PDEs. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T10_wave_alpha4_beta3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T10_helmholtz_n4, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P11(self): """ Generates Problem 11: **Mixed** (Convection :math:`\\beta=30`, Reaction :math:`\\rho=5`, Wave :math:`\\alpha=4, \\beta=3`). Three-task mixed hyperparameter optimization combining Convection, Reaction, and Wave PDEs. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T11_convection_beta30, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T11_reaction_rho5, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T11_wave_alpha4_beta3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem
[docs] def P12(self): """ Generates Problem 12: **Mixed** (Convection :math:`\\beta=30`, Reaction :math:`\\rho=5`, Wave :math:`\\alpha=4, \\beta=3`, Helmholtz :math:`n=4`). Four-task mixed hyperparameter optimization combining all PDE types: Convection, Reaction, Wave, and Helmholtz. Returns ------- MTOP A Multi-Task Optimization Problem instance. """ problem = MTOP() lower_bound = np.array([2, 5, 0, 5000, 10, 1e-5]) upper_bound = np.array([10, 100, 5, 100000, 200, 0.1]) problem.add_task(PINN_HPO.T12_convection_beta30, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T12_reaction_rho5, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T12_wave_alpha4_beta3, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) problem.add_task(PINN_HPO.T12_helmholtz_n4, dim=6, lower_bound=lower_bound, upper_bound=upper_bound) return problem