docs: def setup_objective(obj, free_variables, on_step=None, disp=True, make_dense=False):
      '''
      obj here can be a list of ch objects or a dict of label: ch objects. Either way, the ch
      objects will be merged into one objective using a ChInputsStacked. The labels are just used
      for printing out values per objective with each iteration. If make_dense is True, the
      resulting object with return a desne Jacobian
      '''
      # Validate free variables
      num_unique_ids = len(np.unique(np.array([id(freevar) for freevar in free_variables])))
      if num_unique_ids != len(free_variables):
          raise Exception('The "free_variables" param contains duplicate variables.')
      # Extract labels
      labels = {}
      if isinstance(obj, list) or isinstance(obj, tuple):
          obj = ch.concatenate([f.ravel() for f in obj])
      elif isinstance(obj, dict):
          labels = obj
          obj = ch.concatenate([f.ravel() for f in list(obj.values())])
      # build objective
      x = np.concatenate([freevar.r.ravel() for freevar in free_variables])
      obj = ChInputsStacked(obj=obj, free_variables=free_variables, x=x, make_dense=make_dense)
      # build callback
      def callback():
          if on_step is not None:
              on_step(obj)
          if disp:
              report_line = ['%.2e' % (np.sum(obj.r**2),)]
              for label, objective in sorted(list(labels.items()), key=lambda x: x[0]):
                  report_line.append('%s: %.2e' % (label, np.sum(objective.r**2)))
              report_line = " | ".join(report_line) + '\n'
              sys.stderr.write(report_line)
      return obj, callback