Error plotting 4D parameter 'conv1.weight': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.
Error plotting 1D parameter 'conv1.bias': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.   
Error plotting 2D parameter 'fc1.weight': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.   
Error plotting 1D parameter 'fc1.bias': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.     
Error plotting 2D parameter 'fc2.weight': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.   
Error plotting 1D parameter 'fc2.bias': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.     
Error plotting 2D parameter 'fc3.weight': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead.   
Error plotting 1D parameter 'fc3.bias': Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead. 
import os
import torch
import plotly.express as px
from plotly.graph_objs import Figure
from torch.nn import Module
from typing import Tuple, Any, Optional


def create_visualization_directory(directory: str) -> None:
    """
    Creates the visualization directory if it does not exist.

    Args:
        directory (str): Path to the directory.
    """
    try:
        os.makedirs(directory, exist_ok=True)
    except OSError as e:
        raise RuntimeError(f"Failed to create directory {directory}: {e}")


def save_plot(fig: Figure, filepath: str) -> None:
    """
    Saves the Plotly figure to an HTML file.

    Args:
        fig (Figure): Plotly figure object.
        filepath (str): Path to save the HTML file.
    """
    try:
        fig.write_html(filepath)
    except Exception as e:
        raise RuntimeError(f"Failed to save plot to {filepath}: {e}")


def plot_1d(param_name: str, param_tensor: torch.Tensor, output_dir: str) -> None:
    """
    Creates and saves a box plot for 1D tensor.

    Args:
        param_name (str): Name of the parameter.
        param_tensor (torch.Tensor): 1D tensor.
        output_dir (str): Directory to save the plot.
    """
    try:
        data = {
            'Values': param_tensor.cpu().numpy()
        }
        fig = px.box(data, y='Values',
                     title=f'Box Plot for {param_name} | Shape: {param_tensor.shape}',
                     labels={'Values': 'Parameter Values'})
        filepath = os.path.join(output_dir, f"{param_name}_box_plot.html")
        save_plot(fig, filepath)
    except Exception as e:
        print(f"Error plotting 1D parameter '{param_name}': {e}")


def plot_2d(param_name: str, param_tensor: torch.Tensor, output_dir: str) -> None:
    """
    Creates and saves a scatter plot for 2D tensor.

    Args:
        param_name (str): Name of the parameter.
        param_tensor (torch.Tensor): 2D tensor.
        output_dir (str): Directory to save the plot.
    """
    try:
        if param_tensor.shape[0] < 2:
            raise ValueError("2D tensor must have at least two elements in the first dimension.")

        data = {
            'X': param_tensor.cpu().numpy()[:, 0],
            'Y': param_tensor.cpu().numpy()[:, 1]
        }
        fig = px.scatter(data, x='X', y='Y',
                         title=f'Scatter Plot for {param_name} | Shape: {param_tensor.shape}',
                         labels={'X': 'Dimension 1', 'Y': 'Dimension 2'})
        filepath = os.path.join(output_dir, f"{param_name}_scatter_plot.html")
        save_plot(fig, filepath)
    except Exception as e:
        print(f"Error plotting 2D parameter '{param_name}': {e}")


def plot_3d(param_name: str, param_tensor: torch.Tensor, output_dir: str) -> None:
    """
    Creates and saves a 3D scatter plot for 3D tensor.

    Args:
        param_name (str): Name of the parameter.
        param_tensor (torch.Tensor): 3D tensor.
        output_dir (str): Directory to save the plot.
    """
    try:
        if param_tensor.ndim != 3:
            raise ValueError("Tensor is not 3D.")

        data = {
            'X': param_tensor.cpu().numpy().reshape(-1, 3)[:, 0],
            'Y': param_tensor.cpu().numpy().reshape(-1, 3)[:, 1],
            'Z': param_tensor.cpu().numpy().reshape(-1, 3)[:, 2]
        }
        fig = px.scatter_3d(data, x='X', y='Y', z='Z',
                            title=f'3D Scatter Plot for {param_name} | Shape: {param_tensor.shape}',
                            labels={'X': 'Dimension 1', 'Y': 'Dimension 2', 'Z': 'Dimension 3'})
        filepath = os.path.join(output_dir, f"{param_name}_3d_scatter_plot.html")
        save_plot(fig, filepath)
    except Exception as e:
        print(f"Error plotting 3D parameter '{param_name}': {e}")


def plot_4d(param_name: str, param_tensor: torch.Tensor, output_dir: str) -> None:
    """
    Creates and saves a 3D scatter plot with color representing the 4th dimension for 4D tensor.

    Args:
        param_name (str): Name of the parameter.
        param_tensor (torch.Tensor): 4D tensor.
        output_dir (str): Directory to save the plot.
    """
    try:
        if param_tensor.ndim != 4:
            raise ValueError("Tensor is not 4D.")

        reshaped = param_tensor.view(-1, 4).cpu().numpy()
        data = {
            'X': reshaped[:, 0],
            'Y': reshaped[:, 1],
            'Z': reshaped[:, 2],
            'Color': reshaped[:, 3]
        }
        fig = px.scatter_3d(data, x='X', y='Y', z='Z',
                            color='Color',
                            title=f'4D Scatter Plot for {param_name} | Shape: {param_tensor.shape}',
                            labels={'X': 'Dimension 1', 'Y': 'Dimension 2', 'Z': 'Dimension 3', 'Color': 'Dimension 4'})
        filepath = os.path.join(output_dir, f"{param_name}_4d_scatter_plot.html")
        save_plot(fig, filepath)
    except Exception as e:
        print(f"Error plotting 4D parameter '{param_name}': {e}")


def plot_5d(param_name: str, param_tensor: torch.Tensor, output_dir: str) -> None:
    """
    Creates and saves a 3D scatter plot with color and size representing the 4th and 5th dimensions for 5D tensor.

    Args:
        param_name (str): Name of the parameter.
        param_tensor (torch.Tensor): 5D tensor.
        output_dir (str): Directory to save the plot.
    """
    try:
        if param_tensor.ndim != 5:
            raise ValueError("Tensor is not 5D.")

        reshaped = param_tensor.view(-1, 5).cpu().numpy()
        data = {
            'X': reshaped[:, 0],
            'Y': reshaped[:, 1],
            'Z': reshaped[:, 2],
            'Color': reshaped[:, 3],
            'Size': reshaped[:, 4]
        }
        fig = px.scatter_3d(data, x='X', y='Y', z='Z',
                            color='Color', size='Size',
                            title=f'5D Scatter Plot for {param_name} | Shape: {param_tensor.shape}',
                            labels={
                                'X': 'Dimension 1',
                                'Y': 'Dimension 2',
                                'Z': 'Dimension 3',
                                'Color': 'Dimension 4',
                                'Size': 'Dimension 5'
                            })
        filepath = os.path.join(output_dir, f"{param_name}_5d_scatter_plot.html")
        save_plot(fig, filepath)
    except Exception as e:
        print(f"Error plotting 5D parameter '{param_name}': {e}")


def visualize_model_parameters(model: Module, output_dir: str = "model_parameter_plots") -> None:
    """
    Visualizes parameters of a PyTorch model based on their dimensionality.

    Args:
        model (Module): The PyTorch model.
        output_dir (str, optional): Directory to save the plots. Defaults to "model_parameter_plots".
    """
    create_visualization_directory(output_dir)

    for name, param in model.named_parameters():
        if not param.requires_grad:
            continue  # Skip non-trainable parameters

        shape = param.shape
        ndim = param.ndimension()

        try:
            if ndim == 1:
                plot_1d(name, param, output_dir)
            elif ndim == 2:
                plot_2d(name, param, output_dir)
            elif ndim == 3:
                plot_3d(name, param, output_dir)
            elif ndim == 4:
                plot_4d(name, param, output_dir)
            elif ndim == 5:
                plot_5d(name, param, output_dir)
            else:
                print(f"Skipping parameter '{name}' with unsupported dimensions: {ndim}D")
        except Exception as e:
            print(f"Failed to plot parameter '{name}': {e}")


# Example Usage
if __name__ == "__main__":
    import torch.nn as nn

    class SampleModel(nn.Module):
        def __init__(self):
            super(SampleModel, self).__init__()
            self.conv1 = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3)
            self.fc1 = nn.Linear(16 * 6 * 6, 120)
            self.fc2 = nn.Linear(120, 84)
            self.fc3 = nn.Linear(84, 10)

        def forward(self, x: torch.Tensor) -> torch.Tensor:
            x = torch.relu(self.conv1(x))
            x = x.view(x.size(0), -1)
            x = torch.relu(self.fc1(x))
            x = torch.relu(self.fc2(x))
            x = self.fc3(x)
            return x

    # Instantiate the model
    model = SampleModel()

    # Visualize the model parameters
    visualize_model_parameters(model, output_dir="model_parameter_plots")