


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 		lowercase_  (     _UpperCamelCase  ):


			'''simple docstring'''







			__lowercase 					=   []
			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 		lowercase_  (     _UpperCamelCase       ,			_UpperCamelCase  ):


			'''simple docstring'''







			__lowercase 					=   []
			for d in reversed(_UpperCamelCase  ):
						idx.append(flat_idx % d  )
						__lowercase 					=   flat_idx // d

			return tuple(reversed(_UpperCamelCase  )  )


@torch.jit.ignore
def 		lowercase_  (     _UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase = None       ,			_UpperCamelCase = None       ,			):


			'''simple docstring'''







			def reduce_edge_list(_UpperCamelCase  ) -> None:
						__lowercase 					=   True
						for i in range(len(_UpperCamelCase  )  ):
									__lowercase 					=   -1 * (i + 1)
									l[reversed_idx] &= tally
									__lowercase 					=   l[reversed_idx]

			if start_edges is None:
						__lowercase 					=   [s == 0 for s in start]
						reduce_edge_list(_UpperCamelCase  )
			if end_edges is None:
						__lowercase 					=   [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  ),)]

			__lowercase 					=   []
			__lowercase 					=   []

			# 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

			__lowercase 					=   tuple(_UpperCamelCase  )
			__lowercase 					=   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

						__lowercase 					=   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

						__lowercase 					=   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()  )
						__lowercase 					=   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 		lowercase_  (     _UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase  ):


			'''simple docstring'''







			__lowercase 					=   t.shape[:no_batch_dims]
			__lowercase 					=   list(_flat_idx_to_idx(_UpperCamelCase       ,			_UpperCamelCase  )  )
			# _get_minimal_slice_set is inclusive
			__lowercase 					=   list(_flat_idx_to_idx(flat_end - 1       ,			_UpperCamelCase  )  )

			# Get an ordered list of slices to perform
			__lowercase 					=   _get_minimal_slice_set(
			    _UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase       ,			)

			__lowercase 					=   [t[s] for s in slices]

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


def 		lowercase_  (     _UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase       ,			_UpperCamelCase = False       ,			_UpperCamelCase = None       ,			_UpperCamelCase = False       ,			):


			'''simple docstring'''







			if not (len(_UpperCamelCase  ) > 0):
						raise ValueError('''Must provide at least one input'''  )

			__lowercase 					=   [shape[:no_batch_dims] for shape in _fetch_dims(_UpperCamelCase  )]
			__lowercase 					=   tuple([max(_UpperCamelCase  ) for s in zip(*_UpperCamelCase  )]  )

			def _prep_inputs(_UpperCamelCase  ) -> torch.Tensor:
						if not low_mem:
									if not sum(t.shape[:no_batch_dims]  ) == no_batch_dims:
												__lowercase 					=   t.expand(orig_batch_dims + t.shape[no_batch_dims:]  )
									__lowercase 					=   t.reshape(-1       ,			*t.shape[no_batch_dims:]  )
						else:
									__lowercase 					=   t.expand(orig_batch_dims + t.shape[no_batch_dims:]  )
						return t

			__lowercase 					=   tensor_tree_map(_prep_inputs       ,			_UpperCamelCase  )
			__lowercase 					=   None
			if _out is not None:
						__lowercase 					=   tensor_tree_map(lambda _UpperCamelCase  : t.view([-1] + list(t.shape[no_batch_dims:]  )  )       ,			_out  )

			__lowercase 					=   1
			for d in orig_batch_dims:
						flat_batch_dim *= d

			__lowercase 					=   flat_batch_dim // chunk_size + (flat_batch_dim % chunk_size != 0)

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

			__lowercase 					=   0
			__lowercase 					=   prepped_outputs
			for _ in range(_UpperCamelCase  ):
						# Chunk the input
						if not low_mem:
									__lowercase 					=   _select_chunk
						else:
									__lowercase 					=   partial(
									    _chunk_slice       ,			flat_start=_UpperCamelCase       ,			flat_end=min(_UpperCamelCase       ,			i + chunk_size  )       ,			no_batch_dims=len(_UpperCamelCase  )       ,			)

						__lowercase 					=   tensor_tree_map(_UpperCamelCase       ,			_UpperCamelCase  )

						# Run the layer on the chunk
						__lowercase 					=   layer(**_UpperCamelCase  )

						# Allocate space for the output
						if out is None:
									__lowercase 					=   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       ,			_UpperCamelCase  ) -> 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:
																					__lowercase 					=   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:
															__lowercase 					=   xa
						elif isinstance(_UpperCamelCase       ,			torch.Tensor  ):
									if _add_into_out:
												out[i : i + chunk_size] += output_chunk
									else:
												__lowercase 					=   output_chunk
						else:
									raise ValueError('''Not supported'''  )

						i += chunk_size

			__lowercase 					=   tensor_tree_map(lambda _UpperCamelCase  : t.view(orig_batch_dims + t.shape[1:]  )       ,			_UpperCamelCase  )

			return out





class 	lowerCamelCase_       :



					'''simple docstring'''







					def __init__(				self ,       snake_case_ = 5_1_2 ,       )   ->			int:







								'''simple docstring'''

								__lowercase 					=   max_chunk_size
								__lowercase 					=   None
								__lowercase 					=   None






					def A      (				self ,       snake_case_ ,       snake_case_ ,       snake_case_						)   ->			int:







								'''simple docstring'''

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

								if min_chunk_size >= self.max_chunk_size:
											return min_chunk_size

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

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

								__lowercase 					=   0
								__lowercase 					=   len(snake_case_						) - 1
								while i > min_viable_chunk_size_index:
											__lowercase 					=   test_chunk_size(candidates[i]						)
											if not viable:
														__lowercase 					=   (min_viable_chunk_size_index + i) // 2
											else:
														__lowercase 					=   i
														__lowercase 					=   (i + len(snake_case_						) - 1) // 2

								return candidates[min_viable_chunk_size_index]






					def A      (				self ,       snake_case_ ,       snake_case_						)   ->			bool:







								'''simple docstring'''

								__lowercase 					=   True
								for aa, aa in zip(snake_case_ ,       snake_case_						):
											assert type(snake_case_						) == type(snake_case_						)
											if isinstance(snake_case_ ,       (list, tuple)						):
														consistent &= self._compare_arg_caches(snake_case_ ,       snake_case_						)
											elif isinstance(snake_case_ ,       snake_case_						):
														__lowercase 					=   [v for _, v in sorted(aa.items() ,       key=lambda snake_case_						: x[0]						)]
														__lowercase 					=   [v for _, v in sorted(aa.items() ,       key=lambda snake_case_						: x[0]						)]
														consistent &= self._compare_arg_caches(snake_case_ ,       snake_case_						)
											else:
														consistent &= aa == aa

								return consistent






					def A      (				self ,       snake_case_ ,       snake_case_ ,       snake_case_ ,       )   ->			int:







								'''simple docstring'''

								__lowercase 					=   True
								__lowercase 					=   tree_map(lambda snake_case_						: a.shape if isinstance(snake_case_ ,       torch.Tensor						) else a ,       snake_case_ ,       snake_case_						)
								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(snake_case_						)
											__lowercase 					=   self._compare_arg_caches(self.cached_arg_data ,       snake_case_						)
								else:
											# Otherwise, we can reuse the precomputed value
											__lowercase 					=   False

								if not consistent:
											__lowercase 					=   self._determine_favorable_chunk_size(
											    snake_case_ ,       snake_case_ ,       snake_case_ ,       )
											__lowercase 					=   arg_data

								assert self.cached_chunk_size is not None

								return self.cached_chunk_size






def 		lowercase_  (     _UpperCamelCase       ,			_UpperCamelCase  ):


			'''simple docstring'''







			return price * (1 + tax_rate)


if __name__ == "__main__":
	print(f'''{price_plus_tax(100, 0.25) = }''')
	print(f'''{price_plus_tax(125.50, 0.05) = }''')


