def aupimo_scores(
    anomaly_maps: torch.Tensor,
    masks: torch.Tensor,
    num_thresholds: int = 300_000,
    fpr_bounds: tuple[float, float] = (1e-5, 1e-4),
    force: bool = False,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, int]:
    """Compute PIMO curves and their Area Under the Curve (AUPIMO) scores.

    AUPIMO scores are computed by integrating PIMO curves within specified FPR
    bounds and normalizing to [0,1]. The score represents the average TPR within
    the FPR bounds.

    Args:
        anomaly_maps: Anomaly score maps of shape ``(N, H, W)`` where:
            - ``N``: number of images
            - ``H``: image height
            - ``W``: image width
        masks: Binary ground truth masks of shape ``(N, H, W)``
        num_thresholds: Number of thresholds ``K`` to compute
        fpr_bounds: Lower and upper bounds of FPR integration range
        force: Whether to force computation despite bad conditions

    Returns:
        tuple containing:
            - thresholds: Shape ``(K,)`` in ascending order
            - shared_fpr: Shape ``(K,)`` in descending order
            - per_image_tprs: Shape ``(N, K)`` in descending order
            - image_classes: Shape ``(N,)`` with values 0 (normal) or 1
              (anomalous)
            - aupimo_scores: Shape ``(N,)`` in range [0,1]
            - num_points: Number of points used in AUC integration

    Raises:
        ValueError: If inputs are invalid
        RuntimeError: If PIMO curves are invalid or integration range has too few
            points

    Example:
        >>> anomaly_maps = torch.rand(10, 32, 32)  # 10 images of 32x32
        >>> masks = torch.randint(0, 2, (10, 32, 32))  # Binary masks
        >>> results = aupimo_scores(
        ...     anomaly_maps,
        ...     masks,
        ...     num_thresholds=100,
        ...     fpr_bounds=(1e-5, 1e-4)
        ... )
        >>> thresholds, shared_fpr, tprs, classes, scores, n_points = results
    """
    _validate.is_rate_range(fpr_bounds)

    # other validations are done in the `pimo` function
    thresholds, shared_fpr, per_image_tprs, image_classes = pimo_curves(
        anomaly_maps=anomaly_maps,
        masks=masks,
        num_thresholds=num_thresholds,
    )
    try:
        _validate.is_valid_threshold(thresholds)
        _validate.is_rate_curve(shared_fpr, nan_allowed=False, decreasing=True)
        _validate.is_images_classes(image_classes)
        _validate.is_per_image_rate_curves(per_image_tprs[image_classes == 1], nan_allowed=False, decreasing=True)

    except ValueError as ex:
        msg = f"Cannot compute AUPIMO because the PIMO curves are invalid. Cause: {ex}"
        raise RuntimeError(msg) from ex

    fpr_lower_bound, fpr_upper_bound = fpr_bounds

    # get the threshold indices where the fpr bounds are achieved
    fpr_lower_bound_thresh_idx, _, fpr_lower_bound_defacto = thresh_at_shared_fpr_level(
        thresholds,
        shared_fpr,
        fpr_lower_bound,
    )
    fpr_upper_bound_thresh_idx, _, fpr_upper_bound_defacto = thresh_at_shared_fpr_level(
        thresholds,
        shared_fpr,
        fpr_upper_bound,
    )

    if not torch.isclose(
        fpr_lower_bound_defacto,
        torch.tensor(fpr_lower_bound, dtype=fpr_lower_bound_defacto.dtype, device=fpr_lower_bound_defacto.device),
        rtol=(rtol := 1e-2),
    ):
        logger.warning(
            "The lower bound of the shared FPR integration range is not exactly "
            f"achieved. Expected {fpr_lower_bound} but got "
            f"{fpr_lower_bound_defacto}, which is not within {rtol=}.",
        )

    if not torch.isclose(
        fpr_upper_bound_defacto,
        torch.tensor(fpr_upper_bound, dtype=fpr_upper_bound_defacto.dtype, device=fpr_upper_bound_defacto.device),
        rtol=rtol,
    ):
        logger.warning(
            "The upper bound of the shared FPR integration range is not exactly "
            f"achieved. Expected {fpr_upper_bound} but got "
            f"{fpr_upper_bound_defacto}, which is not within {rtol=}.",
        )

    # reminder: fpr lower/upper bound is threshold upper/lower bound (reversed)
    thresh_lower_bound_idx = fpr_upper_bound_thresh_idx
    thresh_upper_bound_idx = fpr_lower_bound_thresh_idx

    # deal with edge cases
    if thresh_lower_bound_idx >= thresh_upper_bound_idx:
        msg = (
            "The thresholds corresponding to the given `fpr_bounds` are not "
            "valid because they matched the same threshold or the are in the "
            "wrong order. FPR upper/lower = threshold lower/upper = "
            f"{thresh_lower_bound_idx} and {thresh_upper_bound_idx}."
        )
        raise RuntimeError(msg)

    # limit the curves to the integration range [lbound, ubound]
    shared_fpr_bounded: torch.Tensor = shared_fpr[thresh_lower_bound_idx : (thresh_upper_bound_idx + 1)]
    per_image_tprs_bounded: torch.Tensor = per_image_tprs[:, thresh_lower_bound_idx : (thresh_upper_bound_idx + 1)]

    # `shared_fpr` and `tprs` are in descending order; `flip()` reverts to
    # ascending order
    shared_fpr_bounded = torch.flip(shared_fpr_bounded, dims=[0])
    per_image_tprs_bounded = torch.flip(per_image_tprs_bounded, dims=[1])

    # the log's base does not matter because it's a constant factor canceled by
    # normalization factor
    shared_fpr_bounded_log = torch.log(shared_fpr_bounded)

    # deal with edge cases
    invalid_shared_fpr = ~torch.isfinite(shared_fpr_bounded_log)

    if invalid_shared_fpr.all():
        msg = (
            "Cannot compute AUPIMO because the shared fpr integration range is "
            "invalid). Try increasing the number of thresholds."
        )
        raise RuntimeError(msg)

    if invalid_shared_fpr.any():
        logger.warning(
            "Some values in the shared fpr integration range are nan. "
            "The AUPIMO will be computed without these values.",
        )

        # get rid of nan values by removing them from the integration range
        shared_fpr_bounded_log = shared_fpr_bounded_log[~invalid_shared_fpr]
        per_image_tprs_bounded = per_image_tprs_bounded[:, ~invalid_shared_fpr]

    num_points_integral = int(shared_fpr_bounded_log.shape[0])

    if num_points_integral <= 30:
        msg = (
            "Cannot compute AUPIMO because the shared fpr integration range "
            f"doesn't have enough points. Found {num_points_integral} points in "
            "the integration range. Try increasing `num_thresholds`."
        )
        if not force:
            raise RuntimeError(msg)
        msg += " Computation was forced!"
        logger.warning(msg)

    if num_points_integral < 300:
        logger.warning(
            "The AUPIMO may be inaccurate because the shared fpr integration "
            f"range doesn't have enough points. Found {num_points_integral} "
            "points in the integration range. Try increasing `num_thresholds`.",
        )

    aucs: torch.Tensor = torch.trapezoid(per_image_tprs_bounded, x=shared_fpr_bounded_log, axis=1)

    # normalize, then clip(0, 1) makes sure that the values are in [0, 1] in
    # case of numerical errors
    normalization_factor = aupimo_normalizing_factor(fpr_bounds)
    aucs = (aucs / normalization_factor).clip(0, 1)

    return (thresholds, shared_fpr, per_image_tprs, image_classes, aucs, num_points_integral)
Compute PIMO curves and their Area Under the Curve (AUPIMO) scores.

    AUPIMO scores are computed by integrating PIMO curves within specified FPR
    bounds and normalizing to [0,1]. The score represents the average TPR within
    the FPR bounds.

    Args:
        anomaly_maps: Anomaly score maps of shape ``(N, H, W)`` where:
            - ``N``: number of images
            - ``H``: image height
            - ``W``: image width
        masks: Binary ground truth masks of shape ``(N, H, W)``
        num_thresholds: Number of thresholds ``K`` to compute
        fpr_bounds: Lower and upper bounds of FPR integration range
        force: Whether to force computation despite bad conditions

    Returns:
        tuple containing:
            - thresholds: Shape ``(K,)`` in ascending order
            - shared_fpr: Shape ``(K,)`` in descending order
            - per_image_tprs: Shape ``(N, K)`` in descending order
            - image_classes: Shape ``(N,)`` with values 0 (normal) or 1
              (anomalous)
            - aupimo_scores: Shape ``(N,)`` in range [0,1]
            - num_points: Number of points used in AUC integration

    Raises:
        ValueError: If inputs are invalid
        RuntimeError: If PIMO curves are invalid or integration range has too few
            points

    Example:
        >>> anomaly_maps = torch.rand(10, 32, 32)  # 10 images of 32x32
        >>> masks = torch.randint(0, 2, (10, 32, 32))  # Binary masks
        >>> results = aupimo_scores(
        ...     anomaly_maps,
        ...     masks,
        ...     num_thresholds=100,
        ...     fpr_bounds=(1e-5, 1e-4)
        ... )
        >>> thresholds, shared_fpr, tprs, classes, scores, n_points = results
    
src/anomalib/metrics/pimo/functional.py