(
        self,
        inputs_: List[ArrayIndexType],
        output_: ArrayIndexType,
        size_dict: Dict[str, int],
        memory_limit: Optional[int] = None,
    )
return self.path
**Parameters:**

        - **input_sets** - *(list)* List of sets that represent the lhs side of the einsum subscript
        - **output_set** - *(set)* Set that represents the rhs side of the overall einsum subscript
        - **idx_dict** - *(dictionary)* Dictionary of index sizes
        - **memory_limit** - *(int)* The maximum number of elements in a temporary array

        **Returns:**

        - **path** - *(list)* The contraction order within the memory limit constraint.

        **Examples:**

        ```python
        isets = [set('abd'), set('ac'), set('bdc')]
        oset = set('')
        idx_sizes = {'a': 1, 'b':2, 'c':3, 'd':4}
        optimal(isets, oset, idx_sizes, 5000)
        #> [(0, 2), (0, 1)]
def __call__(
        self,
        inputs_: List[ArrayIndexType],
        output_: ArrayIndexType,
        size_dict: Dict[str, int],
        memory_limit: Optional[int] = None,
    ) -> PathType:
        """

        **Parameters:**

        - **input_sets** - *(list)* List of sets that represent the lhs side of the einsum subscript
        - **output_set** - *(set)* Set that represents the rhs side of the overall einsum subscript
        - **idx_dict** - *(dictionary)* Dictionary of index sizes
        - **memory_limit** - *(int)* The maximum number of elements in a temporary array

        **Returns:**

        - **path** - *(list)* The contraction order within the memory limit constraint.

        **Examples:**

        ```python
        isets = [set('abd'), set('ac'), set('bdc')]
        oset = set('')
        idx_sizes = {'a': 1, 'b':2, 'c':3, 'd':4}
        optimal(isets, oset, idx_sizes, 5000)
        #> [(0, 2), (0, 1)]
        """
        self._check_args_against_first_call(inputs_, output_, size_dict)

        inputs: Tuple[FrozenSet[str]] = tuple(map(frozenset, inputs_))  # type: ignore
        output: FrozenSet[str] = frozenset(output_)

        size_cache = {k: helpers.compute_size_by_dict(k, size_dict) for k in inputs}
        result_cache: Dict[Tuple[FrozenSet[str], FrozenSet[str]], Tuple[FrozenSet[str], int]] = {}

        def _branch_iterate(path, inputs, remaining, flops, size):

            # reached end of path (only ever get here if flops is best found so far)
            if len(remaining) == 1:
                self.best["size"] = size
                self.best["flops"] = flops
                self.best["ssa_path"] = path
                return

            def _assess_candidate(k1: FrozenSet[str], k2: FrozenSet[str], i: int, j: int) -> Any:
                # find resulting indices and flops
                try:
                    k12, flops12 = result_cache[k1, k2]
                except KeyError:
                    k12, flops12 = result_cache[k1, k2] = calc_k12_flops(inputs, output, remaining, i, j, size_dict)

                try:
                    size12 = size_cache[k12]
                except KeyError:
                    size12 = size_cache[k12] = helpers.compute_size_by_dict(k12, size_dict)

                new_flops = flops + flops12
                new_size = max(size, size12)

                # sieve based on current best i.e. check flops and size still better
                if not self.better(new_flops, new_size, self.best["flops"], self.best["size"]):
                    return None

                # compare to how the best method was doing as this point
                if new_flops < self.best_progress[len(inputs)]:
                    self.best_progress[len(inputs)] = new_flops
                # sieve based on current progress relative to best
                elif new_flops > self.cutoff_flops_factor * self.best_progress[len(inputs)]:
                    return None

                # sieve based on memory limit
                if (memory_limit not in _UNLIMITED_MEM) and (size12 > memory_limit):  # type: ignore
                    # terminate path here, but check all-terms contract first
                    new_flops = flops + _compute_oversize_flops(inputs, remaining, output_, size_dict)
                    if new_flops < self.best["flops"]:
                        self.best["flops"] = new_flops
                        self.best["ssa_path"] = path + (tuple(remaining),)
                    return None

                # set cost heuristic in order to locally sort possible contractions
                size1, size2 = size_cache[inputs[i]], size_cache[inputs[j]]
                cost = self.cost_fn(size12, size1, size2, k12, k1, k2)

                return cost, flops12, new_flops, new_size, (i, j), k12

            # check all possible remaining paths
            candidates = []
            for i, j in itertools.combinations(remaining, 2):
                if i > j:
                    i, j = j, i
                k1, k2 = inputs[i], inputs[j]

                # initially ignore outer products
                if k1.isdisjoint(k2):
                    continue

                candidate = _assess_candidate(k1, k2, i, j)
                if candidate:
                    heapq.heappush(candidates, candidate)

            # assess outer products if nothing left
            if not candidates:
                for i, j in itertools.combinations(remaining, 2):
                    if i > j:
                        i, j = j, i
                    k1, k2 = inputs[i], inputs[j]
                    candidate = _assess_candidate(k1, k2, i, j)
                    if candidate:
                        heapq.heappush(candidates, candidate)

            # recurse into all or some of the best candidate contractions
            bi = 0
            while (self.nbranch is None or bi < self.nbranch) and candidates:
                _, _, new_flops, new_size, (i, j), k12 = heapq.heappop(candidates)
                _branch_iterate(
                    path=path + ((i, j),),
                    inputs=inputs + (k12,),
                    remaining=(remaining - {i, j}) | {len(inputs)},
                    flops=new_flops,
                    size=new_size,
                )
                bi += 1

        _branch_iterate(path=(), inputs=inputs, remaining=set(range(len(inputs))), flops=0, size=0)

        return self.path
def __call__ ( self , inputs_ : List [ ArrayIndexType ] , output_ : ArrayIndexType , size_dict : Dict [ str , int ] , memory_limit : Optional [ int ] = None , ) -> PathType : self . _check_args_against_first_call ( inputs_ , output_ , size_dict ) inputs : Tuple [ FrozenSet [ str ] ] = tuple ( map ( frozenset , inputs_ ) ) # type: ignore output : FrozenSet [ str ] = frozenset ( output_ ) size_cache = { k : helpers . compute_size_by_dict ( k , size_dict ) for k in inputs } result_cache : Dict [ Tuple [ FrozenSet [ str ] , FrozenSet [ str ] ] , Tuple [ FrozenSet [ str ] , int ] ] = { } def _branch_iterate ( path , inputs , remaining , flops , size ) : # reached end of path (only ever get here if flops is best found so far) if len ( remaining ) == 1 : self . best [ "size" ] = size self . best [ "flops" ] = flops self . best [ "ssa_path" ] = path return def _assess_candidate ( k1 : FrozenSet [ str ] , k2 : FrozenSet [ str ] , i : int , j : int ) -> Any : # find resulting indices and flops try : k12 , flops12 = result_cache [ k1 , k2 ] except KeyError : k12 , flops12 = result_cache [ k1 , k2 ] = calc_k12_flops ( inputs , output , remaining , i , j , size_dict ) try : size12 = size_cache [ k12 ] except KeyError : size12 = size_cache [ k12 ] = helpers . compute_size_by_dict ( k12 , size_dict ) new_flops = flops + flops12 new_size = max ( size , size12 ) # sieve based on current best i.e. check flops and size still better if not self . better ( new_flops , new_size , self . best [ "flops" ] , self . best [ "size" ] ) : return None # compare to how the best method was doing as this point if new_flops < self . best_progress [ len ( inputs ) ] : self . best_progress [ len ( inputs ) ] = new_flops # sieve based on current progress relative to best elif new_flops > self . cutoff_flops_factor * self . best_progress [ len ( inputs ) ] : return None # sieve based on memory limit if ( memory_limit not in _UNLIMITED_MEM ) and ( size12 > memory_limit ) : # type: ignore # terminate path here, but check all-terms contract first new_flops = flops + _compute_oversize_flops ( inputs , remaining , output_ , size_dict ) if new_flops < self . best [ "flops" ] : self . best [ "flops" ] = new_flops self . best [ "ssa_path" ] = path + ( tuple ( remaining ) , ) return None # set cost heuristic in order to locally sort possible contractions size1 , size2 = size_cache [ inputs [ i ] ] , size_cache [ inputs [ j ] ] cost = self . cost_fn ( size12 , size1 , size2 , k12 , k1 , k2 ) return cost , flops12 , new_flops , new_size , ( i , j ) , k12 # check all possible remaining paths candidates = [ ] for i , j in itertools . combinations ( remaining , 2 ) : if i > j : i , j = j , i k1 , k2 = inputs [ i ] , inputs [ j ] # initially ignore outer products if k1 . isdisjoint ( k2 ) : continue candidate = _assess_candidate ( k1 , k2 , i , j ) if candidate : heapq . heappush ( candidates , candidate ) # assess outer products if nothing left if not candidates : for i , j in itertools . combinations ( remaining , 2 ) : if i > j : i , j = j , i k1 , k2 = inputs [ i ] , inputs [ j ] candidate = _assess_candidate ( k1 , k2 , i , j ) if candidate : heapq . heappush ( candidates , candidate ) # recurse into all or some of the best candidate contractions bi = 0 while ( self . nbranch is None or bi < self . nbranch ) and candidates : _ , _ , new_flops , new_size , ( i , j ) , k12 = heapq . heappop ( candidates ) _branch_iterate ( path = path + ( ( i , j ) , ) , inputs = inputs + ( k12 , ) , remaining = ( remaining - { i , j } ) | { len ( inputs ) } , flops = new_flops , size = new_size , ) bi += 1 _branch_iterate ( path = ( ) , inputs = inputs , remaining = set ( range ( len ( inputs ) ) ) , flops = 0 , size = 0 ) return self . path