docs: def bisect(func, a, b, xtol=1e-6, errorcontrol=True,
             testkwargs=dict(), outside='extrapolate',
             ascending=None,
             disp=False):
      """Find root by bysection search.
  
      If the function evaluation is noisy then use `errorcontrol=True` for adaptive
      sampling of the function during the bisection search.
  
      Parameters
      ----------
      func: callable
          Function of which the root should be found. If `errorcontrol=True`
          then the function should be derived from `AverageBase`.
      a, b: float
          initial interval
      xtol: float
          target tolerance for interval size
      errorcontrol: boolean
          if true, assume that function is derived from `AverageBase`.
      testkwargs: only for `errorcontrol=True`
          see `AverageBase.test0`
      outside: ['extrapolate', 'raise']
          How to handle the case where f(a) and f(b) have same sign,
          i.e. where the root lies outside of the interval.
          If 'raise' throws a `BisectException`.
      ascending: allow passing in directly whether function is ascending or not
          if ascending=True then it is assumed without check that f(a) < 0 and f(b) > 0
          if ascending=False then it is assumed without check that f(a) > 0 and f(b) < 0
  
      Returns
      -------
      float, root of function
      """
      search = True
      # check whether function is ascending or not
      if ascending is None:
          if errorcontrol:
              testkwargs.update(dict(type_='smaller', force=True))
              fa = func.test0(a, **testkwargs)
              fb = func.test0(b, **testkwargs)
          else:
              fa = func(a) < 0
              fb = func(b) < 0
          if fa and not fb:
              ascending = True
          elif fb and not fa:
              ascending =  False
          else:
              if disp:
                  print('Warning: func(a) and func(b) do not have opposing signs -> no search done')
              if outside == 'raise':
                  raise BisectException()
              search = False
  
      # refine interval until it has reached size xtol, except if root outside
      while (b-a > xtol) and search:
          mid = (a+b)/2.0
          if ascending:
              if ((not errorcontrol) and (func(mid) < 0)) or \
                      (errorcontrol and func.test0(mid, **testkwargs)):
                  a = mid 
              else:
                  b = mid
          else:
              if ((not errorcontrol) and (func(mid) < 0)) or \
                      (errorcontrol and func.test0(mid, **testkwargs)):
                  b = mid 
              else:
                  a = mid
          if disp:
              print('bisect bounds', a, b)
      # interpolate linearly to get zero
      if errorcontrol:
          ya, yb = func(a)[0], func(b)[0]
      else:
          ya, yb = func(a), func(b)
      m = (yb-ya) / (b-a)
      res = a-ya/m
      if disp:
          print('bisect final value', res)
      return res