Following this instruction :
1. Strictly adhere to PEP-8 standards, ensuring code is clean, well-structured, and follows Python's style guide for readability.
2. Use appropriate modules like `typing` for type hints, ensuring code clarity, maintainability, and leveraging Python's standard libraries effectively.
3. Focus on time, space, robustness, scalability; write optimized, efficient code that can handle large datasets and scale seamlessly.
4. Implement comprehensive error handling, catching exceptions gracefully, providing meaningful error messages, and ensuring code robustness under unexpected conditions.
5. Ensure the developer agent writes advanced, maintainable Python code, balancing performance and readability while adhering to best practices.

TASK : Generalised robustness metrics Pipeline (logits or predictions or loss values)

NOTE : using the only torch module we have implement below scratch 

Problem Statement:


5. Image Generation
Inception Score (IS): Evaluates the quality of generated images based on how distinguishable they are.
Frechet Inception Distance (FID): Measures the distance between feature vectors calculated for real and generated images.
Perceptual Similarity: Measures how similar generated images are to real images based on perceptual metrics.
Mean Squared Error (MSE): Average squared difference between generated and real images.
Structural Similarity Index (SSIM): Measures the similarity between two images.
Diversity Score: Evaluates how diverse the generated images are.
Visual Turing Test: Human evaluation of whether generated images can be distinguished from real ones.
Mode Collapse Rate: Frequency of repeated or similar generated images.
Quality Metrics: Evaluates quality based on specific application requirements.
Training Stability: Analyzes the consistency of training and convergence.

ERROR:__main__:Error computing Frechet Inception Distance: linalg.cholesky: The factorization could not be completed because the input is not positive-definite (the leading minor of order 2 is not positive-definite).
Traceback (most recent call last):
  File "c:\Users\heman\Desktop\Notes\metrics\computervision\Imagegeneration.py", line 395, in <module>
    fid_score = pipeline.frechet_inception_distance(real_imgs, gen_imgs)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\heman\Desktop\Notes\metrics\computervision\Imagegeneration.py", line 70, in frechet_inception_distance
    fid = self._calculate_frechet_distance(mu1, sigma1, mu2, sigma2)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "c:\Users\heman\Desktop\Notes\metrics\computervision\Imagegeneration.py", line 307, in _calculate_frechet_distance
    covmean = torch.linalg.cholesky(sigma1 @ sigma2)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
torch._C._LinAlgError: linalg.cholesky: The factorization could not be completed because the input is not positive-definite (the leading minor of order 2 is not positive-definite).

import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Tuple, List, Optional
import math
import logging

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class RobustnessMetricsPipeline:
    """
    A pipeline to compute various robustness metrics for image generation.
    """

    def __init__(self, device: Optional[torch.device] = None) -> None:
        """
        Initializes the pipeline.

        Args:
            device (torch.device, optional): The device to run computations on.
                Defaults to CUDA if available, else CPU.
        """
        self.device = device if device else torch.device('cuda' if torch.cuda.is_available() else 'cpu')
        logger.info(f"Using device: {self.device}")

    def inception_score(self, images: torch.Tensor, splits: int = 10) -> float:
        """
        Computes the Inception Score (IS) for the given images.

        Args:
            images (torch.Tensor): Generated images tensor of shape (N, C, H, W).
            splits (int): Number of splits.

        Returns:
            float: The Inception Score.
        """
        try:
            # Placeholder for Inception model predictions
            # In practice, replace with a pre-trained Inception model
            preds = self._fake_inception_predictions(images)

            # Compute the marginal distribution
            p_y = torch.mean(preds, dim=0, keepdim=True)
            kl_div = preds * (torch.log(preds) - torch.log(p_y))
            sum_kl_div = torch.sum(kl_div, dim=1)
            is_score = torch.exp(torch.mean(sum_kl_div)).item()
            logger.info(f"Inception Score: {is_score}")
            return is_score
        except Exception as e:
            logger.error(f"Error computing Inception Score: {e}")
            raise

    def frechet_inception_distance(self, real_images: torch.Tensor, generated_images: torch.Tensor) -> float:
        """
        Computes the Frechet Inception Distance (FID) between real and generated images.

        Args:
            real_images (torch.Tensor): Real images tensor of shape (N, C, H, W).
            generated_images (torch.Tensor): Generated images tensor of shape (N, C, H, W).

        Returns:
            float: The FID score.
        """
        try:
            # Placeholder for feature extraction
            mu1, sigma1 = self._compute_statistics(real_images)
            mu2, sigma2 = self._compute_statistics(generated_images)
            fid = self._calculate_frechet_distance(mu1, sigma1, mu2, sigma2)
            logger.info(f"Frechet Inception Distance: {fid}")
            return fid
        except Exception as e:
            logger.error(f"Error computing Frechet Inception Distance: {e}")
            raise

    def perceptual_similarity(self, real_images: torch.Tensor, generated_images: torch.Tensor) -> float:
        """
        Computes perceptual similarity between real and generated images.

        Args:
            real_images (torch.Tensor): Real images tensor of shape (N, C, H, W).
            generated_images (torch.Tensor): Generated images tensor of shape (N, C, H, W).

        Returns:
            float: The perceptual similarity score.
        """
        try:
            similarity = F.cosine_similarity(
                real_images.view(real_images.size(0), -1),
                generated_images.view(generated_images.size(0), -1)
            ).mean().item()
            logger.info(f"Perceptual Similarity: {similarity}")
            return similarity
        except Exception as e:
            logger.error(f"Error computing Perceptual Similarity: {e}")
            raise

    def mean_squared_error(self, real_images: torch.Tensor, generated_images: torch.Tensor) -> float:
        """
        Computes Mean Squared Error (MSE) between real and generated images.

        Args:
            real_images (torch.Tensor): Real images tensor.
            generated_images (torch.Tensor): Generated images tensor.

        Returns:
            float: The MSE value.
        """
        try:
            mse = F.mse_loss(generated_images, real_images).item()
            logger.info(f"Mean Squared Error: {mse}")
            return mse
        except Exception as e:
            logger.error(f"Error computing Mean Squared Error: {e}")
            raise

    def structural_similarity_index(self, real_images: torch.Tensor, generated_images: torch.Tensor) -> float:
        """
        Computes Structural Similarity Index (SSIM) between real and generated images.

        Args:
            real_images (torch.Tensor): Real images tensor.
            generated_images (torch.Tensor): Generated images tensor.

        Returns:
            float: The SSIM value.
        """
        try:
            # Parameters for SSIM
            C1 = 0.01 ** 2
            C2 = 0.03 ** 2

            real_mean = self._gaussian_filter(real_images)
            gen_mean = self._gaussian_filter(generated_images)

            real_var = self._variance_filter(real_images, real_mean)
            gen_var = self._variance_filter(generated_images, gen_mean)
            covariance = self._covariance_filter(real_images, generated_images, real_mean, gen_mean)

            ssim_map = ((2 * real_mean * gen_mean + C1) * (2 * covariance + C2)) / (
                (real_mean ** 2 + gen_mean ** 2 + C1) * (real_var + gen_var + C2)
            )
            ssim = ssim_map.mean().item()
            logger.info(f"Structural Similarity Index: {ssim}")
            return ssim
        except Exception as e:
            logger.error(f"Error computing Structural Similarity Index: {e}")
            raise

    def diversity_score(self, generated_images: torch.Tensor) -> float:
        """
        Computes Diversity Score of generated images.

        Args:
            generated_images (torch.Tensor): Generated images tensor.

        Returns:
            float: The diversity score.
        """
        try:
            norms = F.normalize(generated_images.view(generated_images.size(0), -1), dim=1)
            similarity_matrix = torch.matmul(norms, norms.t())
            # Exclude self-similarity by subtracting identity matrix
            similarity_matrix = similarity_matrix - torch.eye(similarity_matrix.size(0)).to(self.device)
            diversity = 1 - similarity_matrix.mean().item()
            logger.info(f"Diversity Score: {diversity}")
            return diversity
        except Exception as e:
            logger.error(f"Error computing Diversity Score: {e}")
            raise

    def mode_collapse_rate(self, generated_images: torch.Tensor, threshold: float = 0.99) -> float:
        """
        Computes Mode Collapse Rate of generated images.

        Args:
            generated_images (torch.Tensor): Generated images tensor.
            threshold (float): Similarity threshold to consider images as collapsed.

        Returns:
            float: The mode collapse rate.
        """
        try:
            norms = F.normalize(generated_images.view(generated_images.size(0), -1), dim=1)
            similarity_matrix = torch.matmul(norms, norms.t())
            similarity_matrix = similarity_matrix > threshold
            # Count number of similar pairs excluding self-comparison
            collapse_pairs = similarity_matrix.sum().item() - generated_images.size(0)
            total_pairs = generated_images.size(0) * (generated_images.size(0) - 1)
            mode_collapse = collapse_pairs / total_pairs if total_pairs > 0 else 0.0
            logger.info(f"Mode Collapse Rate: {mode_collapse}")
            return mode_collapse
        except Exception as e:
            logger.error(f"Error computing Mode Collapse Rate: {e}")
            raise

    def training_stability(self, loss_values: List[float]) -> float:
        """
        Analyzes training stability based on loss values.

        Args:
            loss_values (List[float]): List of loss values during training.

        Returns:
            float: The training stability metric (e.g., variance).
        """
        try:
            if not loss_values:
                raise ValueError("Loss values list is empty.")
            loss_tensor = torch.tensor(loss_values, device=self.device)
            variance = loss_tensor.var().item()
            logger.info(f"Training Stability (Variance of Loss): {variance}")
            return variance
        except Exception as e:
            logger.error(f"Error computing Training Stability: {e}")
            raise

    def quality_metrics(self, images: torch.Tensor, reference: torch.Tensor) -> dict:
        """
        Evaluates quality metrics based on specific application requirements.

        Args:
            images (torch.Tensor): Generated images tensor.
            reference (torch.Tensor): Reference images tensor.

        Returns:
            dict: A dictionary of quality metrics.
        """
        try:
            mse = self.mean_squared_error(reference, images)
            ssim = self.structural_similarity_index(reference, images)
            perceptual = self.perceptual_similarity(reference, images)
            quality = {
                'MSE': mse,
                'SSIM': ssim,
                'Perceptual_Similarity': perceptual
            }
            logger.info(f"Quality Metrics: {quality}")
            return quality
        except Exception as e:
            logger.error(f"Error computing Quality Metrics: {e}")
            raise

    def _fake_inception_predictions(self, images: torch.Tensor) -> torch.Tensor:
        """
        Placeholder function for Inception model predictions.

        Args:
            images (torch.Tensor): Images tensor.

        Returns:
            torch.Tensor: Fake predictions tensor.
        """
        # Simulate predictions as random probabilities
        num_classes = 1000
        N = images.size(0)
        preds = F.softmax(torch.randn(N, num_classes).to(self.device), dim=1)
        return preds

    def _compute_statistics(self, images: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        """
        Computes the mean and covariance of features for FID.

        Args:
            images (torch.Tensor): Images tensor.

        Returns:
            Tuple[torch.Tensor, torch.Tensor]: Mean and covariance.
        """
        features = self._extract_features(images)
        mu = torch.mean(features, dim=0)
        sigma = torch_cov(features, features)
        return mu, sigma

    def _extract_features(self, images: torch.Tensor) -> torch.Tensor:
        """
        Extracts features from images.

        Args:
            images (torch.Tensor): Images tensor.

        Returns:
            torch.Tensor: Features tensor.
        """
        # Placeholder for feature extraction
        # In practice, use a pre-trained network like Inception
        features = images.view(images.size(0), -1).to(self.device)
        return features

    def _calculate_frechet_distance(
        self, mu1: torch.Tensor, sigma1: torch.Tensor, mu2: torch.Tensor, sigma2: torch.Tensor
    ) -> float:
        """
        Calculates the Frechet Distance between two Gaussians.

        Args:
            mu1 (torch.Tensor): Mean of the first distribution.
            sigma1 (torch.Tensor): Covariance of the first distribution.
            mu2 (torch.Tensor): Mean of the second distribution.
            sigma2 (torch.Tensor): Covariance of the second distribution.

        Returns:
            float: The Frechet Distance.
        """
        diff = mu1 - mu2
        covmean = torch.linalg.cholesky(sigma1 @ sigma2)
        fid = diff.dot(diff) + torch.trace(sigma1 + sigma2 - 2 * covmean).item()
        return fid

    def _gaussian_filter(self, images: torch.Tensor, kernel_size: int = 11, sigma: float = 1.5) -> torch.Tensor:
        """
        Applies a Gaussian filter to the images.

        Args:
            images (torch.Tensor): Images tensor.
            kernel_size (int): Size of the Gaussian kernel.
            sigma (float): Standard deviation.

        Returns:
            torch.Tensor: Filtered images.
        """
        channels = images.size(1)
        x = torch.arange(kernel_size).to(self.device)
        x = x - kernel_size // 2
        gauss = torch.exp(-x**2 / (2 * sigma**2))
        gauss = gauss / gauss.sum()
        gauss = gauss.view(1, 1, -1)

        padding = kernel_size // 2
        images = images.view(-1, 1, images.size(2), images.size(3))
        filtered = F.conv2d(images, gauss.expand(channels, 1, -1), padding=padding, groups=channels)
        return filtered.view(images.size(0)//channels, channels, images.size(2), images.size(3))

    def _variance_filter(self, images: torch.Tensor, mean: torch.Tensor) -> torch.Tensor:
        """
        Computes variance of images using a Gaussian filter.

        Args:
            images (torch.Tensor): Images tensor.
            mean (torch.Tensor): Mean of the images.

        Returns:
            torch.Tensor: Variance tensor.
        """
        variance = self._gaussian_filter(images ** 2) - mean ** 2
        return variance

    def _covariance_filter(
        self, images1: torch.Tensor, images2: torch.Tensor, mean1: torch.Tensor, mean2: torch.Tensor
    ) -> torch.Tensor:
        """
        Computes covariance between two sets of images.

        Args:
            images1 (torch.Tensor): First set of images.
            images2 (torch.Tensor): Second set of images.
            mean1 (torch.Tensor): Mean of the first set.
            mean2 (torch.Tensor): Mean of the second set.

        Returns:
            torch.Tensor: Covariance tensor.
        """
        covariance = self._gaussian_filter(images1 * images2) - mean1 * mean2
        return covariance

def torch_cov(x: torch.Tensor, y: torch.Tensor) -> torch.Tensor:
    """
    Computes the covariance matrix between two tensors.

    Args:
        x (torch.Tensor): Tensor of shape (N, D).
        y (torch.Tensor): Tensor of shape (N, D).

    Returns:
        torch.Tensor: Covariance matrix of shape (D, D).
    """
    N, D = x.size()
    mean_x = torch.mean(x, dim=0)
    mean_y = torch.mean(y, dim=0)
    cov = (x - mean_x).t() @ (y - mean_y) / (N - 1)
    return cov

# Example Usage
if __name__ == "__main__":
    pipeline = RobustnessMetricsPipeline()

    # Generate dummy data
    real_imgs = torch.randn(100, 3, 64, 64).to(pipeline.device)
    gen_imgs = torch.randn(100, 3, 64, 64).to(pipeline.device)
    loss_vals = [0.5 / (i + 1) for i in range(100)]

    # Compute Metrics
    is_score = pipeline.inception_score(gen_imgs)
    fid_score = pipeline.frechet_inception_distance(real_imgs, gen_imgs)
    perceptual_sim = pipeline.perceptual_similarity(real_imgs, gen_imgs)
    mse = pipeline.mean_squared_error(real_imgs, gen_imgs)
    ssim = pipeline.structural_similarity_index(real_imgs, gen_imgs)
    diversity = pipeline.diversity_score(gen_imgs)
    mode_collapse = pipeline.mode_collapse_rate(gen_imgs)
    stability = pipeline.training_stability(loss_vals)
    quality = pipeline.quality_metrics(gen_imgs, real_imgs)

    # Print Metrics
    print(f"Inception Score: {is_score}")
    print(f"FID Score: {fid_score}")
    print(f"Perceptual Similarity: {perceptual_sim}")
    print(f"MSE: {mse}")
    print(f"SSIM: {ssim}")
    print(f"Diversity Score: {diversity}")
    print(f"Mode Collapse Rate: {mode_collapse}")
    print(f"Training Stability: {stability}")
    print(f"Quality Metrics: {quality}")