docs: def aggregate(self, rankings, algorithm, epsilon, max_iters, approx_step, opto="scipy", true_params=None):
          """
          Description:
              Takes in a set of rankings and an algorithm to apply
              and computes the mixing proportions and parameters for
              a mixture of Plackett-Luce models.
          Parameters:
              rankings:    set of rankings over the set of alternatives
              algorithm:   string abbreviation of the moment conditions to use (scipy only)
              epsilon:     convergence condition threshold value (scipy only)
              max_iters:   maximum number of iterations for the optimization (scipy only)
              approx_step: step size for numerical approximation of Jacobian (scipy only)
              opto:        optimization library to use, either "scipy" or "matlab"
          """
          t0 = None
          t1 = None
          t2 = None
          t0 = time.perf_counter() ###################
          if opto.startswith("matlab") and self.matlabEng is None:
              raise ValueError("invalid argument for opto: matlab engine not available")
  
          funcs = None
          try:
              funcs = GMMMixPLAggregator.mixPLalgorithms[algorithm]
          except KeyError:
              raise ValueError("invalid argument value for algorithm: '" + str(algorithm) + "'")
  
          # choose constraints for objective function
          # Aeq = None
          # beq = None
          # if algorithm.endswith("uncons"):
          #     if opto == "scipy":
          #         raise NotImplementedError("unconstrained optimization with scipy not implemented")
          #     else: # opto startswith "matlab"
          #         Aeq = self.Aeq_uncons
          #         beq = self.beq_uncons
          # else: # opto is constrained
          #     Aeq = self.Aeq
          #     beq = self.beq
  
          # compute moment condition values
          moments = None
          mixPLobjective_partial = None
          if opto.startswith("matlab_emp"):
              if true_params is None:
                  raise ValueError("invalid value 'true_params=None' when 'opto=" + opto + "''")
              if algorithm.startswith("top3_full"):
                  moments = calcMomentsMatlabEmpirical(true_params)
              elif algorithm.startswith("top3_min"):
                  moments = calcMomentsMatlabEmpirical_reduced(true_params)
              else:
                  raise NotImplementedError("matlab empirical optimization not implemented for moments: '" + algorithm + "''")
          else: # opto is exactly "scipy" or "matlab"
              moments = funcs.calcMoments(rankings)
              # partial function only used for scipy
              mixPLobjective_partial = functools.partial(funcs.mixPLobjective,
                                                         moments=moments
                                                        )
  
          # generate an initial guess for the optimizer
          params_t0 = np.empty(2*self.m + 1)
          params_t0[0] = np.random.rand()
          params_t0[1:self.m+1] = np.random.dirichlet(np.ones(self.m))
          params_t0[self.m+1:] = np.random.dirichlet(np.ones(self.m))
  
          # optimization
          res = None
          if opto == "scipy":
              res = scipy.optimize.minimize(mixPLobjective_partial,
                                            params_t0,
                                            method="SLSQP",
                                            bounds=self.bounds_pairs,
                                            constraints=self.cons,
                                            options={
                                              'disp': False,
                                              'maxiter': max_iters,
                                              'ftol': epsilon,
                                              'eps': approx_step
                                              }
                                           )
              res = res.x
          elif opto.startswith("matlab"):
              tolfun = 1e-10
              tolx = 1e-10
              tolcon = 1e-8
              if opto.endswith("_default"): # default tolerances for interior-point
                  tolfun = 1e-6
                  tolx = 1e-10
                  tolcon = 1e-6
              elif opto.endswith("_ultra"): # "optimized" tolerances for interior-point
                  tolfun = 1e-13
                  tolx = 1e-13
                  tolcon = 1e-9
  
              moments = matlab.double(moments.tolist())
              params_t0 = matlab.double(params_t0.tolist())
              t1 = time.perf_counter() ###################
              res, val, fl = self.matlabEng.optimize("gmm_mixpl_objectives." + funcs.mixPLobjective.__name__,
                                                     moments,
                                                     params_t0,
                                                     self.A,
                                                     self.b,
                                                     Aeq,
                                                     beq,
                                                     self.lb,
                                                     self.ub,
                                                     {"Algorithm": "interior-point",
                                                      "Display": "off",
                                                      "TolFun": tolfun,
                                                      "TolX": tolx,
                                                      "TolCon": tolcon},
                                                     nargout=3
                                                    )
              t2 = time.perf_counter() ###################
              print("t1", t0)
              res = np.array(res[0])
  
          return (res, .5, .5)