text: # AUTOGENERATED! DO NOT EDIT! File to edit: 02_complex.ipynb (unless otherwise specified).
  
  __all__ = ['make_complex', 'numpy', 'real', 'imag', 'scalar_mult', 'matmul', 'inner_prod', 'outer_prod', 'einsum',
             'conjugate', 'conj', 'elementwise_mult', 'elementwise_division', 'absolute_value', 'kronecker_prod',
             'sigmoid', 'scalar_divide', 'inverse', 'norm_sqr', 'norm', 'I']
  
  # Cell
  import torch
  import numpy as np
  
  
  I = torch.Tensor([0, 1])  # noqa: E741
  
  
  def make_complex(x, y=None):
      if isinstance(x, np.ndarray):
          return make_complex(torch.tensor(x.real), torch.tensor(x.imag)).contiguous()
  
      if y is None:
          y = torch.zeros_like(x)
      return torch.cat((x.unsqueeze(0), y.unsqueeze(0)), dim=0)
  
  
  def numpy(x):
      return real(x).detach().cpu().numpy() + 1j * imag(x).detach().cpu().numpy()
  
  
  def real(x):
      return x[0, ...]
  
  
  def imag(x):
      return x[1, ...]
  
  
  def scalar_mult(x, y, out=None):
      y = y.to(x)
      if out is None:
          out = torch.zeros(2, *((real(x) * real(y)).shape)).to(x)
      else:
          if out is x or out is y:
              raise RuntimeError("Can't overwrite an argument!")
  
      torch.mul(real(x), real(y), out=real(out)).sub_(torch.mul(imag(x), imag(y)))
      torch.mul(real(x), imag(y), out=imag(out)).add_(torch.mul(imag(x), real(y)))
  
      return out
  
  
  def matmul(x, y):
      y = y.to(x)
      re = torch.matmul(real(x), real(y)).sub_(torch.matmul(imag(x), imag(y)))
      im = torch.matmul(real(x), imag(y)).add_(torch.matmul(imag(x), real(y)))
  
      return make_complex(re, im)
  
  
  def inner_prod(x, y):
      y = y.to(x)
  
      if x.dim() == 2 and y.dim() == 2:
          return make_complex(
              torch.dot(real(x), real(y)) + torch.dot(imag(x), imag(y)),
              torch.dot(real(x), imag(y)) - torch.dot(imag(x), real(y)),
          )
      elif x.dim() == 1 and y.dim() == 1:
          return make_complex(
              (real(x) * real(y)) + (imag(x) * imag(y)),
              (real(x) * imag(y)) - (imag(x) * real(y)),
          )
      else:
          raise ValueError("Unsupported input shapes!")
  
  
  def outer_prod(x, y):
      if x.dim() != 2 or y.dim() != 2:
          raise ValueError("An input is not of the right dimension.")
  
      z = torch.zeros(2, x.size()[1], y.size()[1], dtype=x.dtype, device=x.device)
      z[0] = torch.ger(real(x), real(y)) - torch.ger(imag(x), -imag(y))
      z[1] = torch.ger(real(x), -imag(y)) + torch.ger(imag(x), real(y))
  
      return z
  
  
  def einsum(equation, a, b, real_part=True, imag_part=True):
      if real_part:
          r = torch.einsum(equation, real(a), real(b)).sub_(
              torch.einsum(equation, imag(a), imag(b))
          )
      if imag_part:
          i = torch.einsum(equation, real(a), imag(b)).add_(
              torch.einsum(equation, imag(a), real(b))
          )
  
      if real_part and imag_part:
          return make_complex(r, i)
      elif real_part:
          return r
      elif imag_part:
          return i
      else:
          return None
  
  
  def conjugate(x):
      if x.dim() < 3:
          return conj(x)
      else:
          return make_complex(
              torch.transpose(real(x), 0, 1), -torch.transpose(imag(x), 0, 1)
          )
  
  
  def conj(x):
      return make_complex(real(x), -imag(x))
  
  
  def elementwise_mult(x, y):
      return scalar_mult(x, y)
  
  
  def elementwise_division(x, y):
      if x.shape != y.shape:
          raise ValueError("x and y must have the same shape!")
  
      y_star = conj(y)
  
      sqrd_abs_y = absolute_value(y).pow_(2)
  
      return elementwise_mult(x, y_star).div_(sqrd_abs_y)
  
  
  def absolute_value(x):
      x_star = conj(x)
      return real(elementwise_mult(x, x_star)).sqrt_()
  
  
  def kronecker_prod(x, y):
      if not (x.dim() == y.dim() == 3):
          raise ValueError("Inputs must be complex matrices!")
  
      return einsum("ab,cd->acbd", x, y).reshape(
          2, x.shape[1] * y.shape[1], x.shape[2] * y.shape[2]
      )
  
  
  def sigmoid(x, y):
      z = (x.cpu().numpy()) + 1j * (y.cpu().numpy())
  
      out = np.exp(z) / (1 + np.exp(z))
      out = torch.tensor([np.real(out), np.imag(out)]).to(x)
  
      return out
  
  
  def scalar_divide(x, y):
      return scalar_mult(x, inverse(y))
  
  
  def inverse(z):
      z_star = conj(z)
      denominator = real(scalar_mult(z, z_star))
  
      return z_star / denominator
  
  
  def norm_sqr(x):
      return real(inner_prod(x, x))
  
  
  def norm(x):
      return norm_sqr(x).sqrt_()
  