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"

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)