async def find_failing_ops(
        self,
        inputs: Dict[str, np.array],
        validate_output: Callable[[proto.MIL_pb2.Operation, np.array], bool],
        output_names: Optional[List[str]] = None,
        skip_op: Callable[[proto.MIL_pb2.Operation], bool] = skip_op_by_type,
    ) -> List[proto.MIL_pb2.Operation]:
        """
        Identify operations in the model that fail the specified validation criteria.

        This method traverses the model's operation graph, starting from the specified
        output operations, and applies the given validation function to each operation's
        output. It returns a list of operations that are likely the source of failures.

        Parameters
        ----------
        inputs : Dict[str, np.array]
            A dictionary of input tensors for the model. Keys are input names, and values are numpy arrays.

        validate_output : Callable[[proto.MIL_pb2.Operation, np.array], bool]
            A function that takes an operation and its output array, and returns False if the output
            fails the validation criteria, True otherwise.

        output_names : Optional[List[str]]
            A list of specific output names to start the traversal from. If None, all model outputs are used.

        skip_op: Callable[[proto.MIL_pb2.Operation], bool]
            A function that determines if an operation should be skipped.

        Returns
        -------
        List[proto.MIL_pb2.Operation]:
            A list of operations that are identified as likely sources of failure based on the validation criteria.

        Notes
        -----
            - The method uses a breadth-first search strategy to traverse the operation graph.
            - An operation is considered a failure source if it fails validation, but its direct inputs pass.

        Examples
        --------
        .. sourcecode:: python

            validator = coremltools.models.ml_program.experimental.debugging_utils.MLModelValidator(
                model
            )
            failing_ops = await validator.find_failing_ops(
                inputs={"input": np.array([1, 2, 3])},
                validate_output=lambda op, out: not np.isnan(out).any(),
                output_names=["output1", "output2"],
            )
        """

        if output_names is None:
            output_names = [
                output_name for output_name in self._model_inspector.model.output_description
            ]

        queue = []
        all_deps = {}
        for output_name in output_names:
            op = self._output_name_to_op_map.get(output_name, None)
            if op is not None:
                queue.append(op)
                all_deps.update(
                    _get_unique_dependencies(
                        op=op,
                        output_name_to_op_map=self._output_name_to_op_map,
                    )
                )

        result = []
        statuses = {}

        with tqdm.tqdm(total=len(all_deps), desc="\033[Analyzing operations...\033[0m") as pbar:
            while len(queue) > 0:
                op = queue[0]
                status = statuses.get(id(op), None)
                if status is not None:
                    if _is_failure_source(
                        op=op,
                        statuses=statuses,
                        output_name_to_op_map=self._output_name_to_op_map,
                    ):
                        """
                        The operation did not pass validation, but its dependencies did.
                        This suggests that the failure likely comes from this operation.
                        """
                        if op not in result:
                            result.append(op)

                    queue.pop(0)
                else:
                    status = await self._process_op(
                        op=op,
                        inputs=inputs,
                        queue=queue,
                        validate_output=validate_output,
                        skip_op=_skip_op_excluding_output_names(
                            skip_op=skip_op, output_names=output_names
                        ),
                    )
                    output_name = next((output.name for output in op.outputs), "")
                    pbar.set_description(
                        desc=f"\033[1mAnalyzed operation:\033[0m {output_name}, \033[1mtype:\033[0m {op.type}"
                    )
                    pbar.update(n=1)
                statuses[id(op)] = status

        self._outputs = {}
        self._visited = set()

        return result

        Identify operations in the model that fail the specified validation criteria.

        This method traverses the model's operation graph, starting from the specified
        output operations, and applies the given validation function to each operation's
        output. It returns a list of operations that are likely the source of failures.

        Parameters
        ----------
        inputs : Dict[str, np.array]
            A dictionary of input tensors for the model. Keys are input names, and values are numpy arrays.

        validate_output : Callable[[proto.MIL_pb2.Operation, np.array], bool]
            A function that takes an operation and its output array, and returns False if the output
            fails the validation criteria, True otherwise.

        output_names : Optional[List[str]]
            A list of specific output names to start the traversal from. If None, all model outputs are used.

        skip_op: Callable[[proto.MIL_pb2.Operation], bool]
            A function that determines if an operation should be skipped.

        Returns
        -------
        List[proto.MIL_pb2.Operation]:
            A list of operations that are identified as likely sources of failure based on the validation criteria.

        Notes
        -----
            - The method uses a breadth-first search strategy to traverse the operation graph.
            - An operation is considered a failure source if it fails validation, but its direct inputs pass.

        Examples
        --------
        .. sourcecode:: python

            validator = coremltools.models.ml_program.experimental.debugging_utils.MLModelValidator(
                model
            )
            failing_ops = await validator.find_failing_ops(
                inputs={"input": np.array([1, 2, 3])},
                validate_output=lambda op, out: not np.isnan(out).any(),
                output_names=["output1", "output2"],
            )
        
coremltools/models/ml_program/experimental/debugging_utils.py