docs: def full_opt_run(cls, vasp_cmd, vol_change_tol=0.02,
                       max_steps=10, ediffg=-0.05, half_kpts_first_relax=False,
                       **vasp_job_kwargs):
          """
          Returns a generator of jobs for a full optimization run. Basically,
          this runs an infinite series of geometry optimization jobs until the
          % vol change in a particular optimization is less than vol_change_tol.
  
          Args:
              vasp_cmd (str): Command to run vasp as a list of args. For example,
                  if you are using mpirun, it can be something like
                  ["mpirun", "pvasp.5.2.11"]
              vol_change_tol (float): The tolerance at which to stop a run.
                  Defaults to 0.05, i.e., 5%.
              max_steps (int): The maximum number of runs. Defaults to 10 (
                  highly unlikely that this limit is ever reached).
              ediffg (float): Force convergence criteria for subsequent runs (
                  ignored for the initial run.)
              half_kpts_first_relax (bool): Whether to halve the kpoint grid
                  for the first relaxation. Speeds up difficult convergence
                  considerably. Defaults to False.
              \*\*vasp_job_kwargs: Passthrough kwargs to VaspJob. See
                  :class:`custodian.vasp.jobs.VaspJob`.
  
          Returns:
              Generator of jobs.
          """
          for i in range(max_steps):
              if i == 0:
                  settings = None
                  backup = True
                  if half_kpts_first_relax and os.path.exists("KPOINTS") and \
                          os.path.exists("POSCAR"):
                      kpts = Kpoints.from_file("KPOINTS")
                      orig_kpts_dict = kpts.as_dict()
                      kpts.kpts = np.maximum(np.array(kpts.kpts) / 2, 1).tolist()
                      low_kpts_dict = kpts.as_dict()
                      settings = [
                          {"dict": "KPOINTS",
                           "action": {"_set": low_kpts_dict}}
                      ]
              else:
                  backup = False
                  initial = Poscar.from_file("POSCAR").structure
                  final = Poscar.from_file("CONTCAR").structure
                  vol_change = (final.volume - initial.volume) / initial.volume
  
                  logger.info("Vol change = %.1f %%!" % (vol_change * 100))
                  if abs(vol_change) < vol_change_tol:
                      logger.info("Stopping optimization!")
                      break
                  else:
                      incar_update = {"ISTART": 1}
                      if ediffg:
                          incar_update["EDIFFG"] = ediffg
                      settings = [
                          {"dict": "INCAR",
                           "action": {"_set": incar_update}},
                          {"file": "CONTCAR",
                           "action": {"_file_copy": {"dest": "POSCAR"}}}]
                      if i == 1 and half_kpts_first_relax:
                          settings.append({"dict": "KPOINTS",
                                           "action": {"_set": orig_kpts_dict}})
              logger.info("Generating job = %d!" % (i+1))
              yield VaspJob(vasp_cmd, final=False, backup=backup,
                            suffix=".relax%d" % (i+1), settings_override=settings,
                            **vasp_job_kwargs)