docs: def minimize_dogleg(obj, free_variables, on_step=None,
                       maxiter=200, max_fevals=np.inf, sparse_solver='spsolve',
                       disp=True, e_1=1e-15, e_2=1e-15, e_3=0., delta_0=None,
                       treat_as_dense=False):
      """"Nonlinear optimization using Powell's dogleg method.
      See Lourakis et al, 2005, ICCV '05, "Is Levenberg-Marquardt the
      Most Efficient Optimization for Implementing Bundle Adjustment?":
      http://www.ics.forth.gr/cvrl/publications/conferences/0201-P0401-lourakis-levenberg.pdf
  
      e_N are stopping conditions:
      e_1 is gradient magnatude threshold
      e_2 is step size magnatude threshold
      e_3 is improvement threshold (as a ratio; 0.1 means it must improve by 10%% at each step)
  
      maxiter and max_fevals are also stopping conditions. Note that they're not quite the same,
      as an iteration may evaluate the function more than once.
  
      sparse_solver is the solver to use to calculate the Gauss-Newton step in the common case
      that the Jacobian is sparse. It can be 'spsolve' (in which case scipy.sparse.linalg.spsolve
      will be used), 'cg' (in which case scipy.sparse.linalg.cg will be used), or any callable
      that matches the api of scipy.sparse.linalg.spsolve to solve `A x = b` for x where A is sparse.
  
      cg, uses a Conjugate Gradient method, and will be faster if A is sparse but x is dense.
      spsolve will be faster if x is also sparse.
  
      delta_0 defines the initial trust region. Generally speaking, if this is set too low then
      the optimization will never really go anywhere (to small a trust region to make any real
      progress before running out of iterations) and if it's set too high then the optimization
      will diverge immidiately and go wild (such a large trust region that the initial step so
      far overshoots that it can't recover). If it's left as None, it will be automatically
      estimated on the first iteration; it's always updated at each iteration, so this is treated
      only as an initialization.
  
      handle_as_dense explicitly converts all Jacobians of obj to dense matrices
      """
  
  
      solve = setup_sparse_solver(sparse_solver)
      obj, callback = setup_objective(obj, free_variables, on_step=on_step, disp=disp,
                                      make_dense=treat_as_dense)
  
      state = DoglegState(delta=delta_0, solve=solve)
      state.p = obj.x.r
  
      #inject profiler if in DEBUG mode
      if ch.DEBUG:
          from .monitor import DrWrtProfiler
          obj.profiler = DrWrtProfiler(obj)
  
      callback()
      state.updateJ(obj)
      state.r = obj.r
  
      def stop(msg):
          if not state.done:
              pif(msg)
          state.done = True
  
      if np.linalg.norm(state.g, np.inf) < e_1:
          stop('stopping because norm(g, np.inf) < %.2e' % e_1)
      while not state.done:
          state.start_iteration()
          while True:
              state.update_step()
              if state.step_size <= e_2 * np.linalg.norm(state.p):
                  stop('stopping because of small step size (norm_dl < %.2e)' % (e_2 * np.linalg.norm(state.p)))
              else:
                  tm = timer()
                  obj.x = state.p + state.step
                  trial = state.trial_r(obj.r)
                  pif('Residuals computed in %.2fs' % tm())
                  # if the objective function improved, update input parameter estimate.
                  # Note that the obj.x already has the new parms,
                  # and we should not set them again to the same (or we'll bust the cache)
                  if trial.is_improvement:
                      state.p = state.p + state.step
                      callback()
                      if e_3 > 0. and trial.improvement < e_3:
                          stop('stopping because improvement < %.1e%%' % (100*e_3))
                      else:
                          state.updateJ(obj)
                          state.r = trial.r
                          if np.linalg.norm(state.g, np.inf) < e_1:
                              stop('stopping because norm(g, np.inf) < %.2e' % e_1)
                  else:  # Put the old parms back
                      obj.x = ch.Ch(state.p)
                      obj.on_changed('x') # copies from flat vector to free variables
                  # update our trust region
                  state.updateRadius(trial.rho)
                  if state.delta <= e_2*np.linalg.norm(state.p):
                      stop('stopping because trust region is too small')
              if state.done or trial.is_improvement or (obj.fevals >= max_fevals):
                  break
          if state.iteration >= maxiter:
              stop('stopping because max number of user-specified iterations (%d) has been met' % maxiter)
          elif obj.fevals >= max_fevals:
              stop('stopping because max number of user-specified func evals (%d) has been met' % max_fevals)
      return obj.free_variables