



'''simple docstring'''





import logging
import math
from functools import partial
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Tuple, Union

import torch

from .tensor_utils import tensor_tree_map, tree_map





def   a_   (		_UpperCAmelCase    :      Union[dict, list, tuple, torch.Tensor]				)    ->  List[Tuple[int, ...]]:
      __snake_case :   Tuple													=    []
      if isinstance(_UpperCAmelCase ,_UpperCAmelCase				):
            for v in tree.values():
                  shapes.extend(_fetch_dims(_UpperCAmelCase				)				)
      elif isinstance(_UpperCAmelCase ,(list, tuple)				):
            for t in tree:
                  shapes.extend(_fetch_dims(_UpperCAmelCase				)				)
      elif isinstance(_UpperCAmelCase ,torch.Tensor				):
            shapes.append(tree.shape				)
      else:
            raise ValueError('Not supported'				)

      return shapes





@torch.jit.ignore
def   a_   (		_UpperCAmelCase    :      int ,_UpperCAmelCase    :      Tuple[int, ...]				)    ->  Tuple[int, ...]:
      __snake_case :   Optional[Any]													=    []
      for d in reversed(_UpperCAmelCase				):
            idx.append(flat_idx % d				)
            __snake_case :   Any													=    flat_idx // d

      return tuple(reversed(_UpperCAmelCase				)				)





@torch.jit.ignore
def   a_   (		_UpperCAmelCase    :      Sequence[int] ,_UpperCAmelCase    :      Sequence[int] ,_UpperCAmelCase    :      Sequence[int] ,_UpperCAmelCase    :      Optional[Sequence[bool]] = None ,_UpperCAmelCase    :      Optional[Sequence[bool]] = None ,)    ->  List[Tuple[slice, ...]]:

      # start_edges and end_edges both indicate whether, starting from any given
      # dimension, the start/end index is at the top/bottom edge of the
      # corresponding tensor, modeled as a tree
      def reduce_edge_list(_UpperCAmelCase    :      List[bool]				) -> None:
            __snake_case :   Optional[int]													=    True
            for i in range(len(_UpperCAmelCase				)				):
                  __snake_case :   int													=    -1 * (i + 1)
                  l[reversed_idx] &= tally
                  __snake_case :   Optional[int]													=    l[reversed_idx]

      if start_edges is None:
            __snake_case :   Union[str, Any]													=    [s == 0 for s in start]
            reduce_edge_list(_UpperCAmelCase				)
      if end_edges is None:
            __snake_case :   Union[str, Any]													=    [e == (d - 1) for e, d in zip(_UpperCAmelCase ,_UpperCAmelCase				)]
            reduce_edge_list(_UpperCAmelCase				)

      # Base cases. Either start/end are empty and we're done, or the final,
      # one-dimensional tensor can be simply sliced
      if len(_UpperCAmelCase				) == 0:
            return [()]
      elif len(_UpperCAmelCase				) == 1:
            return [(slice(start[0] ,end[0] + 1				),)]

      __snake_case :   List[Tuple[slice, ...]]													=    []
      __snake_case :   List[slice]													=    []

      # Dimensions common to start and end can be selected directly
      for s, e in zip(_UpperCAmelCase ,_UpperCAmelCase				):
            if s == e:
                  path_list.append(slice(_UpperCAmelCase ,s + 1				)				)
            else:
                  break

      __snake_case :   Tuple[slice, ...]													=    tuple(_UpperCAmelCase				)
      __snake_case :   List[Any]													=    len(_UpperCAmelCase				)

      # start == end, and we're done
      if divergence_idx == len(_UpperCAmelCase				):
            return [path]

      def upper() -> Tuple[Tuple[slice, ...], ...]:
            assert start_edges is not None
            assert end_edges is not None

            __snake_case :   List[Any]													=    start[divergence_idx]
            return tuple(
                path + (slice(_UpperCAmelCase ,sdi + 1				),) + s
                for s in _get_minimal_slice_set(
                    start[divergence_idx + 1 :] ,[d - 1 for d in dims[divergence_idx + 1 :]] ,dims[divergence_idx + 1 :] ,start_edges=start_edges[divergence_idx + 1 :] ,end_edges=[True for _ in end_edges[divergence_idx + 1 :]] ,)				)

      def lower() -> Tuple[Tuple[slice, ...], ...]:
            assert start_edges is not None
            assert end_edges is not None

            __snake_case :   str													=    end[divergence_idx]
            return tuple(
                path + (slice(_UpperCAmelCase ,edi + 1				),) + s
                for s in _get_minimal_slice_set(
                    [0 for _ in start[divergence_idx + 1 :]] ,end[divergence_idx + 1 :] ,dims[divergence_idx + 1 :] ,start_edges=[True for _ in start_edges[divergence_idx + 1 :]] ,end_edges=end_edges[divergence_idx + 1 :] ,)				)

      # If both start and end are at the edges of the subtree rooted at
      # divergence_idx, we can just select the whole subtree at once
      if start_edges[divergence_idx] and end_edges[divergence_idx]:
            slices.append(path + (slice(start[divergence_idx] ,end[divergence_idx] + 1				),)				)
      # If just start is at the edge, we can grab almost all of the subtree,
      # treating only the ragged bottom edge as an edge case
      elif start_edges[divergence_idx]:
            slices.append(path + (slice(start[divergence_idx] ,end[divergence_idx]				),)				)
            slices.extend(lower()				)
      # Analogous to the previous case, but the top is ragged this time
      elif end_edges[divergence_idx]:
            slices.extend(upper()				)
            slices.append(path + (slice(start[divergence_idx] + 1 ,end[divergence_idx] + 1				),)				)
      # If both sides of the range are ragged, we need to handle both sides
      # separately. If there's contiguous meat in between them, we can index it
      # in one big chunk
      else:
            slices.extend(upper()				)
            __snake_case :   Dict													=    end[divergence_idx] - start[divergence_idx]
            if middle_ground > 1:
                  slices.append(path + (slice(start[divergence_idx] + 1 ,end[divergence_idx]				),)				)
            slices.extend(lower()				)

      return slices





@torch.jit.ignore
def   a_   (		_UpperCAmelCase    :      torch.Tensor ,_UpperCAmelCase    :      int ,_UpperCAmelCase    :      int ,_UpperCAmelCase    :      int				)    ->  torch.Tensor:
      __snake_case :   Optional[int]													=    t.shape[:no_batch_dims]
      __snake_case :   Any													=    list(_flat_idx_to_idx(_UpperCAmelCase ,_UpperCAmelCase				)				)
      # _get_minimal_slice_set is inclusive
      __snake_case :   Optional[int]													=    list(_flat_idx_to_idx(flat_end - 1 ,_UpperCAmelCase				)				)

      # Get an ordered list of slices to perform
      __snake_case :   Union[str, Any]													=    _get_minimal_slice_set(
          _UpperCAmelCase ,_UpperCAmelCase ,_UpperCAmelCase ,)

      __snake_case :   Any													=    [t[s] for s in slices]

      return torch.cat([s.view((-1,) + t.shape[no_batch_dims:]				) for s in sliced_tensors]				)





def   a_   (		_UpperCAmelCase    :      Callable ,_UpperCAmelCase    :      Dict[str, Any] ,_UpperCAmelCase    :      int ,_UpperCAmelCase    :      int ,_UpperCAmelCase    :      bool = False ,_UpperCAmelCase    :      Any = None ,_UpperCAmelCase    :      bool = False ,)    ->  Any:
      if not (len(_UpperCAmelCase				) > 0):
            raise ValueError('Must provide at least one input'				)

      __snake_case :   int													=    [shape[:no_batch_dims] for shape in _fetch_dims(_UpperCAmelCase				)]
      __snake_case :   Optional[int]													=    tuple([max(_UpperCAmelCase				) for s in zip(*_UpperCAmelCase				)]				)

      def _prep_inputs(_UpperCAmelCase    :      torch.Tensor				) -> torch.Tensor:
            if not low_mem:
                  if not sum(t.shape[:no_batch_dims]				) == no_batch_dims:
                        __snake_case :   Optional[Any]													=    t.expand(orig_batch_dims + t.shape[no_batch_dims:]				)
                  __snake_case :   Optional[Any]													=    t.reshape(-1 ,*t.shape[no_batch_dims:]				)
            else:
                  __snake_case :   List[str]													=    t.expand(orig_batch_dims + t.shape[no_batch_dims:]				)
            return t

      __snake_case :   Dict[str, Any]													=    tensor_tree_map(_prep_inputs ,_UpperCAmelCase				)
      __snake_case :   Any													=    None
      if _out is not None:
            __snake_case :   Any													=    tensor_tree_map(lambda _UpperCAmelCase				: t.view([-1] + list(t.shape[no_batch_dims:]				)				) ,_out				)

      __snake_case :   Union[str, Any]													=    1
      for d in orig_batch_dims:
            flat_batch_dim *= d

      __snake_case :   Any													=    flat_batch_dim // chunk_size + (flat_batch_dim % chunk_size != 0)

      def _select_chunk(_UpperCAmelCase    :      torch.Tensor				) -> torch.Tensor:
            return t[i : i + chunk_size] if t.shape[0] != 1 else t

      __snake_case :   int													=    0
      __snake_case :   List[str]													=    prepped_outputs
      for _ in range(_UpperCAmelCase				):
            # Chunk the input
            if not low_mem:
                  __snake_case :   str													=    _select_chunk
            else:
                  __snake_case :   Tuple													=    partial(
                      _chunk_slice ,flat_start=_UpperCAmelCase ,flat_end=min(_UpperCAmelCase ,i + chunk_size				) ,no_batch_dims=len(_UpperCAmelCase				) ,)

            __snake_case :   Dict[str, Any]													=    tensor_tree_map(_UpperCAmelCase ,_UpperCAmelCase				)

            # Run the layer on the chunk
            __snake_case :   Union[str, Any]													=    layer(**_UpperCAmelCase				)

            # Allocate space for the output
            if out is None:
                  __snake_case :   Union[str, Any]													=    tensor_tree_map(lambda _UpperCAmelCase				: t.new_zeros((flat_batch_dim,) + t.shape[1:]				) ,_UpperCAmelCase				)

            # Put the chunk in its pre-allocated space
            if isinstance(_UpperCAmelCase ,_UpperCAmelCase				):

                  def assign(_UpperCAmelCase    :      dict ,_UpperCAmelCase    :      dict				) -> None:
                        for k, v in da.items():
                              if isinstance(_UpperCAmelCase ,_UpperCAmelCase				):
                                    assign(_UpperCAmelCase ,da[k]				)
                              else:
                                    if _add_into_out:
                                          v[i : i + chunk_size] += da[k]
                                    else:
                                          __snake_case :   List[Any]													=    da[k]

                  assign(_UpperCAmelCase ,_UpperCAmelCase				)
            elif isinstance(_UpperCAmelCase ,_UpperCAmelCase				):
                  for xa, xa in zip(_UpperCAmelCase ,_UpperCAmelCase				):
                        if _add_into_out:
                              xa[i : i + chunk_size] += xa
                        else:
                              __snake_case :   List[str]													=    xa
            elif isinstance(_UpperCAmelCase ,torch.Tensor				):
                  if _add_into_out:
                        out[i : i + chunk_size] += output_chunk
                  else:
                        __snake_case :   Dict													=    output_chunk
            else:
                  raise ValueError('Not supported'				)

            i += chunk_size

      __snake_case :   List[str]													=    tensor_tree_map(lambda _UpperCAmelCase				: t.view(orig_batch_dims + t.shape[1:]				) ,_UpperCAmelCase				)

      return out





class  snake_case__						:
 def __init__(   self						:							List[Any]      ,     __a						:							int = 512      ,     )      ->   Any:







       '''simple docstring'''
       __snake_case :   List[Any]													=    max_chunk_size
       __snake_case :   Optional[int]													=    None
       __snake_case :   Optional[tuple]													=    None
 def A_   (   self						:							Tuple      ,     __a						:							Callable      ,     __a						:							tuple      ,     __a						:							int  )      ->   int:







       '''simple docstring'''
       logging.info('Tuning chunk size...'  )

       if min_chunk_size >= self.max_chunk_size:
             return min_chunk_size

       __snake_case :   List[int]													=    [2**l for l in range(int(math.log(self.max_chunk_size      ,     2  )  ) + 1  )]
       __snake_case :   List[str]													=    [c for c in candidates if c > min_chunk_size]
       __snake_case :   List[str]													=    [min_chunk_size] + candidates
       candidates[-1] += 4

       def test_chunk_size(__a						:							int  ) -> bool:
             try:
                   with torch.no_grad():
                         fn(*__a      ,     chunk_size=__a  )
                   return True
             except RuntimeError:
                   return False

       __snake_case :   Optional[int]													=    0
       __snake_case :   str													=    len(__a  ) - 1
       while i > min_viable_chunk_size_index:
             __snake_case :   str													=    test_chunk_size(candidates[i]  )
             if not viable:
                   __snake_case :   List[str]													=    (min_viable_chunk_size_index + i) // 2
             else:
                   __snake_case :   str													=    i
                   __snake_case :   Optional[int]													=    (i + len(__a  ) - 1) // 2

       return candidates[min_viable_chunk_size_index]
 def A_   (   self						:							List[str]      ,     __a						:							Iterable      ,     __a						:							Iterable  )      ->   bool:







       '''simple docstring'''
       __snake_case :   int													=    True
       for aa, aa in zip(__a      ,     __a  ):
             assert type(__a  ) == type(__a  )
             if isinstance(__a      ,     (list, tuple)  ):
                   consistent &= self._compare_arg_caches(__a      ,     __a  )
             elif isinstance(__a      ,     __a  ):
                   __snake_case :   str													=    [v for _, v in sorted(aa.items()      ,     key=lambda __a  : x[0]  )]
                   __snake_case :   str													=    [v for _, v in sorted(aa.items()      ,     key=lambda __a  : x[0]  )]
                   consistent &= self._compare_arg_caches(__a      ,     __a  )
             else:
                   consistent &= aa == aa

       return consistent





 def A_   (   self						:							Optional[Any]      ,     __a						:							Callable      ,     __a						:							tuple      ,     __a						:							int      ,     )      ->   int:







       '''simple docstring'''
       __snake_case :   Dict													=    True
       __snake_case :   tuple													=    tree_map(lambda __a  : a.shape if isinstance(__a      ,     torch.Tensor  ) else a      ,     __a      ,     __a  )
       if self.cached_arg_data is not None:
             # If args have changed shape/value, we need to re-tune
             assert len(self.cached_arg_data  ) == len(__a  )
             __snake_case :   str													=    self._compare_arg_caches(self.cached_arg_data      ,     __a  )
       else:
             # Otherwise, we can reuse the precomputed value
             __snake_case :   List[str]													=    False

       if not consistent:
             __snake_case :   str													=    self._determine_favorable_chunk_size(
                 __a      ,     __a      ,     __a      ,     )
             __snake_case :   List[Any]													=    arg_data

       assert self.cached_chunk_size is not None

       return self.cached_chunk_size








'''simple docstring'''





import qiskit





def   a_   (		_UpperCAmelCase    :      int = 2				)    ->  qiskit.result.counts.Counts:
      __snake_case :   Union[str, Any]													=    qubits

      # Using Aer's simulator
      __snake_case :   List[Any]													=    qiskit.Aer.get_backend('aer_simulator'				)

      # Creating a Quantum Circuit acting on the q register
      __snake_case :   Dict													=    qiskit.QuantumCircuit(_UpperCAmelCase ,_UpperCAmelCase				)

      # Adding a H gate on qubit 0 (now q0 in superposition)
      circuit.h(0				)

      for i in range(1 ,_UpperCAmelCase				):
            # Adding CX (CNOT) gate
            circuit.cx(i - 1 ,_UpperCAmelCase				)

      # Mapping the quantum measurement to the classical bits
      circuit.measure(list(range(_UpperCAmelCase				)				) ,list(range(_UpperCAmelCase				)				)				)

      # Now measuring any one qubit would affect other qubits to collapse
      # their super position and have same state as the measured one.

      # Executing the circuit on the simulator
      __snake_case :   Optional[Any]													=    qiskit.execute(_UpperCAmelCase ,_UpperCAmelCase ,shots=10_00				)

      return job.result().get_counts(_UpperCAmelCase				)


if __name__ == "__main__":
    print(F"""Total count for various states are: {quantum_entanglement(3)}""")



