コード例 #1
0
class Optimizer(object):
    '''base class for different optimizers'''
    def __init__(self, config, idf, model, simulation_func, world=None):
        # type: (Dict[str, Any], Identification, Model, Callable[[Dict, Trajectory, Model, np._ArrayLike], Tuple[Dict, Data]], str) -> None

        self.config = config
        self.sim_func = simulation_func
        self.model = model
        self.idf = idf
        self.world = world

        self.num_dofs = self.config['num_dofs']

        # optimization status vars
        self.last_best_f = np.inf
        self.last_best_sol = np.array([])

        self.iter_cnt = 0   # iteration counter
        self.last_g = None  # type: List[float]    # last constraint values
        self.is_global = False
        self.local_iter_max = "(unknown)"

        # init parallel runs
        self.parallel = parallel
        if parallel:
            self.mpi_size = nprocs   # number of processes
            self.mpi_rank = comm.Get_rank()  # current process
            self.comm = comm
        else:
            self.mpi_size = 1
            self.mpi_rank = 0

        # init plotting progress
        if self.config['showOptimizationGraph']:
            self.initGraph()

        # init link data
        self.link_cuboid_hulls = {}  # type: Dict[str, List]
        for i in range(self.model.num_links):
            link_name = self.model.linkNames[i]
            box, pos, rot = idf.urdfHelpers.getBoundingBox(
                    input_urdf = self.model.urdf_file,
                    old_com = self.model.xStdModel[i*10+1:i*10+4] / self.model.xStdModel[i*10],
                    link_name = link_name,
                    scaling = False
            )
            self.link_cuboid_hulls[link_name] = [box, pos, rot]

        self.world = world
        self.world_links = []  # type: List[str]
        if world:
            self.world_links = idf.urdfHelpers.getLinkNames(world)
            if self.config['verbose']:
                print('World links: {}'.format(self.world_links))
            for link_name in self.world_links:
                box, pos, rot = idf.urdfHelpers.getBoundingBox(
                        input_urdf = world,
                        old_com = [0,0,0],
                        link_name = link_name,
                        scaling = False
                )
                # make sure no name collision happens
                if link_name not in self.link_cuboid_hulls:
                    self.link_cuboid_hulls[link_name] = [box, pos, rot]
                else:
                    print(Fore.RED+'Warning: link {} declared in model and world file!'.format(link_name) + Fore.RESET)

        self.world_boxes = {link: self.link_cuboid_hulls[link] for link in self.world_links}

        # init some vars for link distance
        vel = [0.0]*self.num_dofs
        self.dq_zero = iDynTree.VectorDynSize.fromList(vel)
        self.world_gravity = iDynTree.SpatialAcc.fromList(self.model.gravity)


    def testBounds(self, x):
        # type: (np._ArrayLike) -> bool
        raise NotImplementedError

    def testConstraints(self, g):
        # type: (np._ArrayLike) -> bool
        raise NotImplementedError

    def getLinkDistance(self, l0_name, l1_name, joint_q):
        # type: (str, str, np._ArrayLike[float]) -> float
        '''get shortest distance from link with id l0 to link with id l1 for posture joint_q'''

        from fcl import fcl, collision_data, transform

        #get link rotation and position in world frame
        q = iDynTree.VectorDynSize.fromList(joint_q)
        self.model.dynComp.setRobotState(q, self.dq_zero, self.dq_zero, self.world_gravity)

        if l0_name in self.model.linkNames:    # if robot link
            f0 = self.model.dynComp.getFrameIndex(l0_name)
            t0 = self.model.dynComp.getWorldTransform(f0)
            rot0 = t0.getRotation().toNumPy()
            pos0 = t0.getPosition().toNumPy()
            s0 = self.config['scaleCollisionHull']
        else:   # if world link
            pos0 = self.link_cuboid_hulls[l0_name][1]
            rot0 = eulerAnglesToRotationMatrix(self.link_cuboid_hulls[l0_name][2])
            s0 = 1

        if l1_name in self.model.linkNames:    # if robot link
            f1 = self.model.dynComp.getFrameIndex(l1_name)
            t1 = self.model.dynComp.getWorldTransform(f1)
            rot1 = t1.getRotation().toNumPy()
            pos1 = t1.getPosition().toNumPy()
            s1 = self.config['scaleCollisionHull']
        else:   # if world link
            pos1 = self.link_cuboid_hulls[l1_name][1]
            rot1 = eulerAnglesToRotationMatrix(self.link_cuboid_hulls[l1_name][2])
            s1 = 1

        # TODO: use pos and rot of boxes for vals from geometry tags
        # self.link_cuboid_hulls[l0_name][1], [2]

        b = np.array(self.link_cuboid_hulls[l0_name][0]) * s0
        p = np.array(self.link_cuboid_hulls[l0_name][1])
        b0_center = 0.5*np.array([(b[1][0])+(b[0][0]) + p[0],
                                  (b[1][1])+(b[0][1]) + p[1],
                                  (b[1][2])+(b[0][2]) + p[2]])
        b0 = fcl.Box(b[1][0]-b[0][0], b[1][1]-b[0][1], b[1][2]-b[0][2])

        b = np.array(self.link_cuboid_hulls[l1_name][0]) * s1
        p = np.array(self.link_cuboid_hulls[l1_name][1])
        b1_center = 0.5*np.array([(b[1][0])+(b[0][0]) + p[0],
                                  (b[1][1])+(b[0][1]) + p[1],
                                  (b[1][2])+(b[0][2]) + p[2]])
        b1 = fcl.Box(b[1][0]-b[0][0], b[1][1]-b[0][1], b[1][2]-b[0][2])

        # move box to pos + box center pos (model has pos in link origin, box has zero at center)
        o0 = fcl.CollisionObject(b0, transform.Transform(rot0, pos0+b0_center))
        o1 = fcl.CollisionObject(b1, transform.Transform(rot1, pos1+b1_center))

        distance, d_result = fcl.distance(o0, o1, collision_data.DistanceRequest(True))

        if distance < 0:
            if self.config['verbose'] > 1:
                print("Collision of {} and {}".format(l0_name, l1_name))

            # get proper collision and depth since optimization should also know how much constraint is violated
            cr = collision_data.CollisionRequest()
            cr.enable_contact = True
            cr.enable_cost = True
            collision, c_result = fcl.collide(o0, o1, cr)

            # sometimes no contact is found even though distance is less than 0?
            if len(c_result.contacts):
                distance = c_result.contacts[0].penetration_depth

        return distance

    def initVisualizer(self):
        if self.config['showModelVisualization'] and self.mpi_rank == 0:
            from visualizer import Visualizer
            self.visualizer = Visualizer(self.config)
            self.visualizer.loadMeshes(self.model.urdf_file, self.model.linkNames, self.idf.urdfHelpers)

            # set draw method for visualizer. This taps into local variables here, a bit unclean...
            def draw_model():
                if self.trajectory:
                    # get data of trajectory
                    self.visualizer.trajectory.setTime(self.visualizer.display_index/self.visualizer.fps)
                    q0 = [self.visualizer.trajectory.getAngle(d) for d in range(self.num_dofs)]
                else:
                    p_id = self.visualizer.display_index
                    q0 = self.visualizer.angles[p_id*self.num_dofs:(p_id+1)*self.num_dofs]

                q = iDynTree.VectorDynSize.fromList(q0)
                dq = iDynTree.VectorDynSize.fromList([0.0]*self.num_dofs)
                self.model.dynComp.setRobotState(q, dq, dq, self.world_gravity)
                self.visualizer.addIDynTreeModel(self.model.dynComp, self.link_cuboid_hulls,
                        self.model.linkNames, self.config['ignoreLinksForCollision'])
                if self.world:
                    self.visualizer.addWorld(self.world_boxes)
                self.visualizer.updateLabels()
            self.visualizer.event_callback = draw_model

    def showVisualizerAngles(self, x):
        '''show visualizer for current joint angles x'''
        if self.config['showModelVisualization'] and self.mpi_rank == 0: #and c:
            self.visualizer.display_max = self.num_postures
            self.visualizer.angles = x
            self.visualizer.event_callback()
            self.visualizer.run()

    def showVisualizerTrajectory(self, t):
        '''show visualizer for joint trajectory t'''
        if self.config['showModelVisualization'] and self.mpi_rank == 0: #and c:
            self.visualizer.setModelTrajectory(t)
            freq = self.config['excitationFrequency']
            self.visualizer.display_max = t.getPeriodLength()*self.visualizer.fps # length of trajectory
            self.visualizer.trajectory = t
            self.visualizer.playable = True
            self.visualizer.event_callback()
            self.visualizer.run()

    def objectiveFunc(self, x, test=False):
        # type: (np._ArrayLike[float], bool) -> Tuple[float, np._ArrayLike, bool]
        ''' calculate objective function and return objective function value f, constraint values g
        and a fail flag'''
        raise NotImplementedError

    def initGraph(self):
        if self.mpi_rank > 0:
            return
        # init graphing of objective function value
        self.fig = plt.figure(0)
        self.ax1 = self.fig.add_subplot(1,1,1)
        plt.ion()
        self.xar = []           # type: List[int]    # x value, i.e. iteration count
        self.yar = []           # type: List[float]  # y value, i.e. obj func value
        self.x_constr = []      # type: List[bool]   # within constraints or not (feasible)
        self.ax1.plot(self.xar, self.yar)
        self.ax1.set_xlabel('Function evaluation #')
        self.ax1.set_ylabel('Objective function value')

        self.updateGraphEveryVals = 5

    def updateGraph(self):
        if self.mpi_rank > 0:
            return
        # draw all optimization steps, mark the ones that are within constraints
        #if (self.iter_cnt % self.updateGraphEveryVals) == 0:
        color = 'g'
        line = self.ax1.plot(self.xar, self.yar, marker='.', markeredgecolor=color, markerfacecolor=color, color="0.75")
        markers = np.where(self.x_constr)[0]
        line[0].set_markevery(list(markers))

        if self.iter_cnt == 1: plt.show(block=False)

        # allow some interaction
        plt.pause(0.01)


    def approx_jacobian(self, f, x, epsilon, *args):
        """Approximate the Jacobian matrix of callable function func

           * Parameters
             x       - The state vector at which the Jacobian matrix is desired
             func    - A vector-valued function of the form f(x,*args)
             epsilon - The peturbation used to determine the partial derivatives
             *args   - Additional arguments passed to func

           * Returns
             An array of dimensions (lenf, lenx) where lenf is the length
             of the outputs of func, and lenx is the number of

           * Notes
             The approximation is done using forward differences
        """

        x0 = np.asfarray(x)
        f0 = f(*((x0,) + args))
        jac = np.zeros((x0.size, f0.size))
        dx = np.zeros(x0.size)
        for i in range(x0.size):
            dx[i] = epsilon
            jac[i] = (f(*((x0 + dx,) + args)) - f0) / epsilon
            dx[i] = 0.0
        return jac.transpose()

    def gather_solutions(self):
        # send best solutions to node 0
        if self.parallel:
            if self.config['verbose']:
                print('Collecting best solutions from processes')
            send_obj = [self.last_best_f, self.last_best_sol, self.mpi_rank]
            received_objs = self.comm.gather(send_obj, root=0)

            #receive solutions from other instances
            if self.mpi_rank == 0:
                for proc in range(0, self.mpi_size):
                    other_best_f, other_best_sol, rank = received_objs[proc]

                    if other_best_f < self.last_best_f:
                        print('received better solution from {}'.format(rank))
                        self.last_best_f = other_best_f
                        self.last_best_sol = other_best_sol

    def runOptimizer(self, opt_prob):
        # type: (pyOpt.Optimization) -> np._ArrayLike[float]
        ''' call global followed by local optimizer, return solution '''

        import pyOpt

        initial = [v.value for v in list(opt_prob.getVarSet().values())]

        if self.config['useGlobalOptimization']:
            ### optimize using pyOpt (global)
            sr = random.SystemRandom()
            if self.config['globalSolver'] == 'NSGA2':
                if parallel:
                    opt = pyOpt.NSGA2(pll_type='POA') # genetic algorithm
                else:
                    opt = pyOpt.NSGA2()
                if self.config['globalOptSize'] % 4:
                    raise IOError("globalOptSize needs to be a multiple of 4 for NSGA2")
                opt.setOption('PopSize', self.config['globalOptSize'])   # Population Size (a Multiple of 4)
                opt.setOption('maxGen', self.config['globalOptIterations'])   # Maximum Number of Generations
                opt.setOption('PrintOut', 0)    # Flag to Turn On Output to files (0-None, 1-Subset, 2-All)
                opt.setOption('xinit', 1)       # Use Initial Solution Flag (0 - random population, 1 - use given solution)
                opt.setOption('seed', sr.random())   # Random Number Seed 0..1 (0 - Auto based on time clock)
                #pCross_real    0.6     Probability of Crossover of Real Variable (0.6-1.0)
                opt.setOption('pMut_real', 0.5)   # Probablity of Mutation of Real Variables (1/nreal)
                #eta_c  10.0    # Distribution Index for Crossover (5-20) must be > 0
                #eta_m  20.0    # Distribution Index for Mutation (5-50) must be > 0
                #pCross_bin     0.0     # Probability of Crossover of Binary Variable (0.6-1.0)
                #pMut_real      0.0     # Probability of Mutation of Binary Variables (1/nbits)
                self.iter_max = self.config['globalOptSize']*self.config['globalOptIterations']
            elif self.config['globalSolver'] == 'ALPSO':
                if parallel:
                    opt = pyOpt.ALPSO(pll_type='SPM')  #augmented lagrange particle swarm optimization
                else:
                    opt = pyOpt.ALPSO()  #augmented lagrange particle swarm optimization
                opt.setOption('stopCriteria', 0)   # stop at max iters
                opt.setOption('dynInnerIter', 1)   # dynamic inner iter number
                opt.setOption('maxInnerIter', 5)
                opt.setOption('maxOuterIter', self.config['globalOptIterations'])
                opt.setOption('printInnerIters', 1)
                opt.setOption('printOuterIters', 1)
                opt.setOption('SwarmSize', self.config['globalOptSize'])
                opt.setOption('xinit', 1)
                opt.setOption('seed', sr.random()*self.mpi_size) #(self.mpi_rank+1)/self.mpi_size)
                #opt.setOption('vcrazy', 1e-2)
                #TODO: how to properly limit max number of function calls?
                # no. func calls = (SwarmSize * inner) * outer + SwarmSize
                self.iter_max = opt.getOption('SwarmSize') * opt.getOption('maxInnerIter') * \
                    opt.getOption('maxOuterIter') + opt.getOption('SwarmSize')
                self.iter_max = self.iter_max // self.mpi_size
            else:
                print("Solver {} not defined".format(self.config['globalSolver']))
                sys.exit(1)

            # run global optimization

            #try:
                #reuse history
            #    opt(opt_prob, store_hst=False, hot_start=True) #, xstart=initial)
            #except NameError:

            if self.config['verbose']:
                print('Running global optimization with {}'.format(self.config['globalSolver']))
            self.is_global = True
            opt(opt_prob, store_hst=False) #, xstart=initial)

            if self.mpi_rank == 0:
                print(opt_prob.solution(0))

            self.gather_solutions()

        ### pyOpt local
        if self.config['useLocalOptimization']:
            print("Runnning local gradient based solver")

            # TODO: run local optimization for e.g. the three last best results (global solutions
            # could be more or less optimal within their local minima)

            # after using global optimization, refine solution with gradient based method init
            # optimizer (more or less local)
            if self.config['localSolver'] == 'SLSQP':
                opt2 = pyOpt.SLSQP()   #sequential least squares
                opt2.setOption('MAXIT', self.config['localOptIterations'])
                if self.config['verbose']:
                    opt2.setOption('IPRINT', 0)
            elif self.config['localSolver'] == 'IPOPT':
                opt2 = pyOpt.IPOPT()
                opt2.setOption('linear_solver', 'ma57')  #mumps or hsl: ma27, ma57, ma77, ma86, ma97 or mkl: pardiso
                opt2.setOption('max_iter', self.config['localOptIterations'])
                if self.config['verbose']:
                    opt2.setOption('print_level', 4)  #0 none ... 5 max
                else:
                    opt2.setOption('print_level', 0)  #0 none ... 5 max
            elif self.config['localSolver'] == 'PSQP':
                opt2 = pyOpt.PSQP()
                opt2.setOption('MIT', self.config['localOptIterations'])  # max iterations
                #opt2.setOption('MFV', ??)  # max function evaluations
            elif self.config['localSolver'] == 'COBYLA':
                if parallel:
                    opt2 = pyOpt.COBYLA(pll_type='POA')
                else:
                    opt2 = pyOpt.COBYLA()
                opt2.setOption('MAXFUN', self.config['localOptIterations'])  # max iterations
                opt2.setOption('RHOBEG', 0.1)  # initial step size
                if self.config['verbose']:
                    opt2.setOption('IPRINT', 2)

            self.iter_max = self.local_iter_max

            # use best constrained solution from last run (might be better than what solver thinks)
            if len(self.last_best_sol) > 0:
                for i in range(len(opt_prob.getVarSet())):
                    opt_prob.getVar(i).value = self.last_best_sol[i]

            if self.config['verbose']:
                print('Runing local optimization with {}'.format(self.config['localSolver']))
            self.is_global = False
            if self.config['localSolver'] in ['COBYLA', 'CONMIN']:
                opt2(opt_prob, store_hst=False)
            else:
                if parallel:
                    opt2(opt_prob, sens_step=0.1, sens_mode='pgc', store_hst=False)
                else:
                    opt2(opt_prob, sens_step=0.1, store_hst=False)

            self.gather_solutions()

        if self.mpi_rank == 0:
            sol = opt_prob.solution(0)
            print(sol)
            #sol_vec = np.array([sol.getVar(x).value for x in range(0,len(sol.getVarSet()))])

            if len(self.last_best_sol) > 0:
                print("using last best constrained solution instead of given solver solution.")

                print("testing final solution")
                #self.iter_cnt = 0
                self.objectiveFunc(self.last_best_sol, test=True)
                print("\n")
                return self.last_best_sol
            else:
                print("No feasible solution found!")
                sys.exit(-1)
        else:
            # parallel sub-processes, close
            sys.exit(0)