From cca357886bc2809d96c2d0a868833f0f9f539a6b Mon Sep 17 00:00:00 2001 From: Shunichi09 Date: Thu, 2 Apr 2020 14:06:43 +0900 Subject: [PATCH 1/3] Remove and Add: Clear all version1.0 files and folders, and Added version2.0 files --- .gitignore | 140 ++- LICENSE | 2 +- PythonLinearNonlinearControl/__init__.py | 0 .../common/__init__.py | 0 PythonLinearNonlinearControl/common/utils.py | 2 + .../configs/__init__.py | 0 .../configs/first_order_lag.py | 92 ++ .../configs/make_configs.py | 9 + .../controllers/__init__.py | 0 .../controllers/cem.py | 135 +++ .../controllers/controller.py | 66 ++ .../controllers/ilqr.py | 24 + .../controllers/make_controllers.py | 18 + .../controllers/mpc.py | 217 +++++ .../controllers/mppi.py | 124 +++ .../controllers/random.py | 77 ++ PythonLinearNonlinearControl/envs/__init__.py | 0 PythonLinearNonlinearControl/envs/cost.py | 37 + PythonLinearNonlinearControl/envs/env.py | 54 ++ .../envs/first_order_lag.py | 113 +++ .../envs/make_envs.py | 8 + .../envs/two_wheeled.py | 145 +++ PythonLinearNonlinearControl/helper.py | 147 +++ .../models/__init__.py | 0 .../models/first_order_lag.py | 51 + .../models/make_models.py | 8 + PythonLinearNonlinearControl/models/model.py | 190 ++++ .../planners/__init__.py | 0 .../planners/const_planner.py | 23 + .../planners/make_planners.py | 8 + .../planners/planner.py | 18 + .../plotters/__init__.py | 0 .../plotters/plot_func.py | 60 ++ .../runners/__init__.py | 0 .../runners/make_runners.py | 4 + .../runners/runner.py | 51 + README.md | 147 ++- ilqr/README.md | 76 -- ilqr/animation.py | 297 ------ ilqr/goal_maker.py | 117 --- ilqr/ilqr.py | 463 --------- ilqr/main_dynamic.py | 66 -- ilqr/main_static.py | 68 -- ilqr/model.py | 131 --- mpc/basic/README.md | 146 --- mpc/basic/animation.py | 233 ----- mpc/basic/main_ACC.py | 246 ----- mpc/basic/main_ACC_TEMP.py | 243 ----- mpc/basic/main_example.py | 188 ---- mpc/basic/mpc_func_with_cvxopt.py | 256 ----- mpc/basic/mpc_func_with_scipy.py | 262 ----- mpc/basic/test_compare_methods.py | 211 ----- mpc/extend/README.md | 41 - mpc/extend/animation.py | 324 ------- mpc/extend/coordinate_trans.py | 112 --- mpc/extend/extended_MPC.py | 306 ------ mpc/extend/func_curvature.py | 182 ---- mpc/extend/main_track.py | 464 --------- mpc/extend/mpc_func_with_cvxopt.py | 304 ------ mpc/extend/traj_func.py | 31 - nmpc/cgmres/README.md | 79 -- nmpc/cgmres/main_example.py | 645 ------------- nmpc/cgmres/main_two_wheeled.py | 893 ------------------ nmpc/newton/README.md | 65 -- nmpc/newton/main_example.py | 657 ------------- scripts/simple_run.py | 53 ++ setup.py | 21 + tests/__init__.py | 0 tests/configs/__init__.py | 0 tests/configs/test_first_order_lag.py | 34 + tests/models/__init__.py | 0 tests/models/test_model.py | 53 ++ 72 files changed, 2105 insertions(+), 7132 deletions(-) mode change 100755 => 100644 LICENSE create mode 100644 PythonLinearNonlinearControl/__init__.py create mode 100644 PythonLinearNonlinearControl/common/__init__.py create mode 100644 PythonLinearNonlinearControl/common/utils.py create mode 100644 PythonLinearNonlinearControl/configs/__init__.py create mode 100644 PythonLinearNonlinearControl/configs/first_order_lag.py create mode 100644 PythonLinearNonlinearControl/configs/make_configs.py create mode 100644 PythonLinearNonlinearControl/controllers/__init__.py create mode 100644 PythonLinearNonlinearControl/controllers/cem.py create mode 100644 PythonLinearNonlinearControl/controllers/controller.py create mode 100644 PythonLinearNonlinearControl/controllers/ilqr.py create mode 100644 PythonLinearNonlinearControl/controllers/make_controllers.py create mode 100644 PythonLinearNonlinearControl/controllers/mpc.py create mode 100644 PythonLinearNonlinearControl/controllers/mppi.py create mode 100644 PythonLinearNonlinearControl/controllers/random.py create mode 100644 PythonLinearNonlinearControl/envs/__init__.py create mode 100644 PythonLinearNonlinearControl/envs/cost.py create mode 100644 PythonLinearNonlinearControl/envs/env.py create mode 100644 PythonLinearNonlinearControl/envs/first_order_lag.py create mode 100644 PythonLinearNonlinearControl/envs/make_envs.py create mode 100644 PythonLinearNonlinearControl/envs/two_wheeled.py create mode 100644 PythonLinearNonlinearControl/helper.py create mode 100644 PythonLinearNonlinearControl/models/__init__.py create mode 100644 PythonLinearNonlinearControl/models/first_order_lag.py create mode 100644 PythonLinearNonlinearControl/models/make_models.py create mode 100644 PythonLinearNonlinearControl/models/model.py create mode 100644 PythonLinearNonlinearControl/planners/__init__.py create mode 100644 PythonLinearNonlinearControl/planners/const_planner.py create mode 100644 PythonLinearNonlinearControl/planners/make_planners.py create mode 100644 PythonLinearNonlinearControl/planners/planner.py create mode 100644 PythonLinearNonlinearControl/plotters/__init__.py create mode 100644 PythonLinearNonlinearControl/plotters/plot_func.py create mode 100644 PythonLinearNonlinearControl/runners/__init__.py create mode 100644 PythonLinearNonlinearControl/runners/make_runners.py create mode 100644 PythonLinearNonlinearControl/runners/runner.py delete mode 100644 ilqr/README.md delete mode 100644 ilqr/animation.py delete mode 100644 ilqr/goal_maker.py delete mode 100644 ilqr/ilqr.py delete mode 100644 ilqr/main_dynamic.py delete mode 100644 ilqr/main_static.py delete mode 100644 ilqr/model.py delete mode 100644 mpc/basic/README.md delete mode 100755 mpc/basic/animation.py delete mode 100644 mpc/basic/main_ACC.py delete mode 100644 mpc/basic/main_ACC_TEMP.py delete mode 100644 mpc/basic/main_example.py delete mode 100644 mpc/basic/mpc_func_with_cvxopt.py delete mode 100644 mpc/basic/mpc_func_with_scipy.py delete mode 100644 mpc/basic/test_compare_methods.py delete mode 100644 mpc/extend/README.md delete mode 100755 mpc/extend/animation.py delete mode 100755 mpc/extend/coordinate_trans.py delete mode 100644 mpc/extend/extended_MPC.py delete mode 100644 mpc/extend/func_curvature.py delete mode 100644 mpc/extend/main_track.py delete mode 100644 mpc/extend/mpc_func_with_cvxopt.py delete mode 100644 mpc/extend/traj_func.py delete mode 100644 nmpc/cgmres/README.md delete mode 100644 nmpc/cgmres/main_example.py delete mode 100644 nmpc/cgmres/main_two_wheeled.py delete mode 100644 nmpc/newton/README.md delete mode 100644 nmpc/newton/main_example.py create mode 100644 scripts/simple_run.py create mode 100644 setup.py create mode 100644 tests/__init__.py create mode 100644 tests/configs/__init__.py create mode 100644 tests/configs/test_first_order_lag.py create mode 100644 tests/models/__init__.py create mode 100644 tests/models/test_model.py diff --git a/.gitignore b/.gitignore index 1c52168..b467fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,134 @@ -*.csv -*.log -*.pickle -*.mp4 +# folders +.vscode/ +.pytest_cache/ +result/ -.cache/ -.eggs/ +# Byte-compiled / optimized / DLL files __pycache__/ -.pytest_cache -cache/ \ No newline at end of file +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/LICENSE b/LICENSE old mode 100755 new mode 100644 index 9a5e36b..0340be2 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Shunichi Sekiguchi +Copyright (c) 2020 Shunichi Sekiguchi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/PythonLinearNonlinearControl/__init__.py b/PythonLinearNonlinearControl/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/common/__init__.py b/PythonLinearNonlinearControl/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/common/utils.py b/PythonLinearNonlinearControl/common/utils.py new file mode 100644 index 0000000..07ff604 --- /dev/null +++ b/PythonLinearNonlinearControl/common/utils.py @@ -0,0 +1,2 @@ +import numpy as np + diff --git a/PythonLinearNonlinearControl/configs/__init__.py b/PythonLinearNonlinearControl/configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/configs/first_order_lag.py b/PythonLinearNonlinearControl/configs/first_order_lag.py new file mode 100644 index 0000000..18f86c9 --- /dev/null +++ b/PythonLinearNonlinearControl/configs/first_order_lag.py @@ -0,0 +1,92 @@ +import numpy as np + +class FirstOrderLagConfigModule(): + # parameters + ENV_NAME = "FirstOrderLag-v0" + TYPE = "Linear" + TASK_HORIZON = 1000 + PRED_LEN = 10 + STATE_SIZE = 4 + INPUT_SIZE = 2 + DT = 0.05 + # cost parameters + R = np.eye(INPUT_SIZE) + Q = np.eye(STATE_SIZE) + Sf = np.eye(STATE_SIZE) + # bounds + INPUT_LOWER_BOUND = np.array([-0.5, -0.5]) + INPUT_UPPER_BOUND = np.array([0.5, 0.5]) + # DT_INPUT_LOWER_BOUND = np.array([-0.5 * DT, -0.5 * DT]) + # DT_INPUT_UPPER_BOUND = np.array([0.25 * DT, 0.25 * DT]) + DT_INPUT_LOWER_BOUND = None + DT_INPUT_UPPER_BOUND = None + + def __init__(self): + """ + Args: + save_dit (str): save directory + """ + # opt configs + self.opt_config = { + "Random": { + "popsize": 5000 + }, + "CEM": { + "popsize": 500, + "num_elites": 50, + "max_iters": 15, + "alpha": 0.3, + "init_var":1., + "threshold":0.001 + }, + "MPPI":{ + "beta" : 0.6, + "popsize": 5000, + "kappa": 0.9, + "noise_sigma": 0.5, + }, + "iLQR":{ + }, + "cgmres-NMPC":{ + }, + "newton-NMPC":{ + }, + } + + @staticmethod + def input_cost_fn(u): + """ input cost functions + Args: + u (numpy.ndarray): input, shape(input_size, ) + or shape(pop_size, input_size) + Returns: + cost (numpy.ndarray): cost of input, none or shape(pop_size, ) + """ + return (u**2) * np.diag(FirstOrderLagConfigModule.R) + + @staticmethod + def state_cost_fn(x, g_x): + """ state cost function + Args: + x (numpy.ndarray): state, shape(pred_len, state_size) + or shape(pop_size, pred_len, state_size) + g_x (numpy.ndarray): goal state, shape(state_size, ) + or shape(pop_size, state_size) + Returns: + cost (numpy.ndarray): cost of state, none or shape(pop_size, ) + """ + return ((x - g_x)**2) * np.diag(FirstOrderLagConfigModule.Q) + + @staticmethod + def terminal_state_cost_fn(terminal_x, terminal_g_x): + """ + Args: + terminal_x (numpy.ndarray): terminal state, + shape(state_size, ) or shape(pop_size, state_size) + terminal_g_x (numpy.ndarray): terminal goal state, + shape(state_size, ) or shape(pop_size, state_size) + Returns: + cost (numpy.ndarray): cost of state, none or shape(pop_size, ) + """ + return ((terminal_x - terminal_g_x)**2) \ + * np.diag(FirstOrderLagConfigModule.Sf) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/configs/make_configs.py b/PythonLinearNonlinearControl/configs/make_configs.py new file mode 100644 index 0000000..fb54981 --- /dev/null +++ b/PythonLinearNonlinearControl/configs/make_configs.py @@ -0,0 +1,9 @@ +from .first_order_lag import FirstOrderLagConfigModule + +def make_config(args): + """ + Returns: + config (ConfigModule class): configuration for the each env + """ + if args.env == "FirstOrderLag": + return FirstOrderLagConfigModule() \ No newline at end of file diff --git a/PythonLinearNonlinearControl/controllers/__init__.py b/PythonLinearNonlinearControl/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/controllers/cem.py b/PythonLinearNonlinearControl/controllers/cem.py new file mode 100644 index 0000000..238ff39 --- /dev/null +++ b/PythonLinearNonlinearControl/controllers/cem.py @@ -0,0 +1,135 @@ +from logging import getLogger + +import numpy as np +import scipy.stats as stats + +from .controller import Controller +from ..envs.cost import calc_cost + +logger = getLogger(__name__) + +class CEM(Controller): + """ Cross Entropy Method for linear and nonlinear method + + Attributes: + history_u (list[numpy.ndarray]): time history of optimal input + Ref: + Chua, K., Calandra, R., McAllister, R., & Levine, S. (2018). + Deep reinforcement learning in a handful of trials + using probabilistic dynamics models. + In Advances in Neural Information Processing Systems (pp. 4754-4765). + """ + def __init__(self, config, model): + super(CEM, self).__init__(config, model) + + # model + self.model = model + + # general parameters + self.pred_len = config.PRED_LEN + self.input_size = config.INPUT_SIZE + + # cem parameters + self.alpha = config.opt_config["CEM"]["alpha"] + self.pop_size = config.opt_config["CEM"]["popsize"] + self.max_iters = config.opt_config["CEM"]["max_iters"] + self.num_elites = config.opt_config["CEM"]["num_elites"] + self.epsilon = config.opt_config["CEM"]["threshold"] + self.init_var = config.opt_config["CEM"]["init_var"] + self.opt_dim = self.input_size * self.pred_len + + # get bound + self.input_upper_bounds = np.tile(config.INPUT_UPPER_BOUND, + self.pred_len) + self.input_lower_bounds = np.tile(config.INPUT_LOWER_BOUND, + self.pred_len) + + # get cost func + self.state_cost_fn = config.state_cost_fn + self.terminal_state_cost_fn = config.terminal_state_cost_fn + self.input_cost_fn = config.input_cost_fn + + # init mean + self.init_mean = np.tile((config.INPUT_UPPER_BOUND \ + + config.INPUT_LOWER_BOUND) / 2., + self.pred_len) + self.prev_sol = self.init_mean.copy() + # init variance + var = np.ones_like(config.INPUT_UPPER_BOUND) \ + * config.opt_config["CEM"]["init_var"] + self.init_var = np.tile(var, self.pred_len) + + # save + self.history_u = [] + + def clear_sol(self): + """ clear prev sol + """ + logger.debug("Clear Sol") + self.prev_sol = self.init_mean.copy() + + def obtain_sol(self, curr_x, g_xs): + """ calculate the optimal inputs + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + g_xs (numpy.ndarrya): goal trajectory, shape(plan_len, state_size) + Returns: + opt_input (numpy.ndarray): optimal input, shape(input_size, ) + """ + # initialize + opt_count = 0 + + # get configuration + mean = self.prev_sol.flatten().copy() + var = self.init_var.flatten().copy() + + # make distribution + X = stats.truncnorm(-1, 1, + loc=np.zeros_like(mean),\ + scale=np.ones_like(mean)) + + while (opt_count < self.max_iters) and np.max(var) > self.epsilon: + # constrained + lb_dist = mean - self.input_lower_bounds + ub_dist = self.input_upper_bounds - mean + constrained_var = np.minimum(np.minimum(np.square(lb_dist), + np.square(ub_dist)), + var) + + # sample + samples = X.rvs(size=[self.pop_size, self.opt_dim]) \ + * np.sqrt(constrained_var) \ + + mean + + # calc cost + # samples.shape = (pop_size, opt_dim) + costs = self.calc_cost(curr_x, + samples.reshape(self.pop_size, + self.pred_len, + self.input_size), + g_xs) + + # sort cost + elites = samples[np.argsort(costs)][:self.num_elites] + + # new mean + new_mean = np.mean(elites, axis=0) + new_var = np.var(elites, axis=0) + + # soft update + mean = self.alpha * mean + (1. - self.alpha) * new_mean + var = self.alpha * var + (1. - self.alpha) * new_var + + logger.debug("Var = {}".format(np.max(var))) + logger.debug("Costs = {}".format(np.mean(costs))) + opt_count += 1 + + sol = mean.copy() + self.prev_sol = np.concatenate((mean[self.input_size:], + np.zeros(self.input_size))) + + return sol.reshape(self.pred_len, self.input_size).copy()[0] + + def __str__(self): + return "CEM" diff --git a/PythonLinearNonlinearControl/controllers/controller.py b/PythonLinearNonlinearControl/controllers/controller.py new file mode 100644 index 0000000..4e63be5 --- /dev/null +++ b/PythonLinearNonlinearControl/controllers/controller.py @@ -0,0 +1,66 @@ +import numpy as np + +from ..envs.cost import calc_cost + +class Controller(): + """ Controller class + """ + def __init__(self, config, model): + """ + """ + self.config = config + self.model = model + + # get cost func + self.state_cost_fn = config.state_cost_fn + self.terminal_state_cost_fn = config.terminal_state_cost_fn + self.input_cost_fn = config.input_cost_fn + + def obtain_sol(self, curr_x, g_xs): + """ calculate the optimal inputs + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + g_xs (numpy.ndarrya): goal trajectory, shape(plan_len, state_size) + Returns: + opt_input (numpy.ndarray): optimal input, shape(input_size, ) + """ + raise NotImplementedError("Implement gradient of hamitonian with respect to the state") + + def calc_cost(self, curr_x, samples, g_xs): + """ calculate the cost of input samples + + Args: + curr_x (numpy.ndarray): shape(state_size), + current robot position + samples (numpy.ndarray): shape(pop_size, opt_dim), + input samples + g_xs (numpy.ndarray): shape(pred_len, state_size), + goal states + Returns: + costs (numpy.ndarray): shape(pop_size, ) + """ + # get size + pop_size = samples.shape[0] + g_xs = np.tile(g_xs, (pop_size, 1, 1)) + + # calc cost, pred_xs.shape = (pop_size, pred_len+1, state_size) + pred_xs = self.model.predict_traj(curr_x, samples) + + # get particle cost + costs = calc_cost(pred_xs, samples, g_xs, + self.state_cost_fn, self.input_cost_fn, \ + self.terminal_state_cost_fn) + + return costs + + @staticmethod + def gradient_hamiltonian_x(x, u, lam): + """ gradient of hamitonian with respect to the state, + """ + raise NotImplementedError("Implement gradient of hamitonian with respect to the state") + + @staticmethod + def gradient_hamiltonian_u(x, u, lam): + """ gradient of hamitonian with respect to the input + """ + raise NotImplementedError("Implement gradient of hamitonian with respect to the input") \ No newline at end of file diff --git a/PythonLinearNonlinearControl/controllers/ilqr.py b/PythonLinearNonlinearControl/controllers/ilqr.py new file mode 100644 index 0000000..46eedac --- /dev/null +++ b/PythonLinearNonlinearControl/controllers/ilqr.py @@ -0,0 +1,24 @@ +from logging import getLogger + +import numpy as np +import scipy.stats as stats + +from .controller import Controller +from ..envs.cost import calc_cost + +logger = getLogger(__name__) + +class iLQR(Controller): + """ iterative Liner Quadratique Regulator + """ + def __init__(self, config, model): + """ + """ + super(iLQR, self).__init__(config, model) + + if config.TYPE != "Nonlinear": + raise ValueError("{} could be not applied to \ + this controller".format(model)) + + self.model = model + \ No newline at end of file diff --git a/PythonLinearNonlinearControl/controllers/make_controllers.py b/PythonLinearNonlinearControl/controllers/make_controllers.py new file mode 100644 index 0000000..05a5f3b --- /dev/null +++ b/PythonLinearNonlinearControl/controllers/make_controllers.py @@ -0,0 +1,18 @@ +from .mpc import LinearMPC +from .cem import CEM +from .random import RandomShooting +from .mppi import MPPI +from .ilqr import iLQR + +def make_controller(args, config, model): + + if args.controller_type == "MPC": + return LinearMPC(config, model) + elif args.controller_type == "CEM": + return CEM(config, model) + elif args.controller_type == "Random": + return RandomShooting(config, model) + elif args.controller_type == "MPPI": + return MPPI(config, model) + elif args.controller_type == "iLQR": + return iLQR(config, model) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/controllers/mpc.py b/PythonLinearNonlinearControl/controllers/mpc.py new file mode 100644 index 0000000..766973c --- /dev/null +++ b/PythonLinearNonlinearControl/controllers/mpc.py @@ -0,0 +1,217 @@ +from logging import getLogger + +import numpy as np +from cvxopt import matrix, solvers + +from .controller import Controller +from ..envs.cost import calc_cost + +logger = getLogger(__name__) + +class LinearMPC(Controller): + """ Model Predictive Controller for linear model + + Attributes: + A (numpy.ndarray): system matrix, shape(state_size, state_size) + B (numpy.ndarray): input matrix, shape(state_size, input_size) + Q (numpy.ndarray): cost function weight for states + R (numpy.ndarray): cost function weight for states + history_us (list[numpy.ndarray]): time history of optimal input + Ref: + Maciejowski, J. M. (2002). Predictive control: with constraints. + """ + def __init__(self, config, model): + """ + Args: + model (Model): system matrix, shape(state_size, state_size) + config (ConfigModule): input matrix, shape(state_size, input_size) + """ + if config.TYPE != "Linear": + raise ValueError("{} could be not applied to \ + this controller".format(model)) + super(LinearMPC, self).__init__(config, model) + # system parameters + self.model = model + self.A = model.A + self.B = model.B + self.state_size = config.STATE_SIZE + self.input_size = config.INPUT_SIZE + self.pred_len = config.PRED_LEN + + # get cost func + self.state_cost_fn = config.state_cost_fn + self.terminal_state_cost_fn = config.terminal_state_cost_fn + self.input_cost_fn = config.input_cost_fn + + # cost parameters + self.Q = config.Q + self.R = config.R + self.Qs = None + self.Rs = None + + # constraints + self.dt_input_lower_bound = config.DT_INPUT_LOWER_BOUND + self.dt_input_upper_bound = config.DT_INPUT_UPPER_BOUND + self.input_lower_bound = config.INPUT_LOWER_BOUND + self.input_upper_bound = config.INPUT_UPPER_BOUND + + # setup controllers + self.W = None + self.omega = None + self.F = None + self.f = None + self.setup() + + # history + self.history_u = [np.zeros(self.input_size)] + + def setup(self): + """ + setup Model Predictive Control as a quadratic programming + """ + A_factorials = [self.A] + self.phi_mat = self.A.copy() + + for _ in range(self.pred_len - 1): + temp_mat = np.matmul(A_factorials[-1], self.A) + self.phi_mat = np.vstack((self.phi_mat, temp_mat)) + A_factorials.append(temp_mat) # after we use this factorials + + self.gamma_mat = self.B.copy() + gammma_mat_temp = self.B.copy() + + for i in range(self.pred_len - 1): + temp_1_mat = np.matmul(A_factorials[i], self.B) + gammma_mat_temp = temp_1_mat + gammma_mat_temp + self.gamma_mat = np.vstack((self.gamma_mat, gammma_mat_temp)) + + self.theta_mat = self.gamma_mat.copy() + + for i in range(self.pred_len - 1): + temp_mat = np.zeros_like(self.gamma_mat) + temp_mat[int((i + 1)*self.state_size): , :] =\ + self.gamma_mat[:-int((i + 1)*self.state_size) , :] + + self.theta_mat = np.hstack((self.theta_mat, temp_mat)) + + # evaluation function weight + diag_Qs = np.tile(np.diag(self.Q), self.pred_len) + diag_Rs = np.tile(np.diag(self.R), self.pred_len) + self.Qs = np.diag(diag_Qs) + self.Rs = np.diag(diag_Rs) + + # constraints + # about inputs + if self.input_lower_bound is not None: + self.F = np.zeros((self.input_size * 2, + self.pred_len * self.input_size)) + + for i in range(self.input_size): + self.F[i * 2: (i + 1) * 2, i] = np.array([1., -1.]) + temp_F = self.F.copy() + + for i in range(self.pred_len - 1): + for j in range(self.input_size): + temp_F[j * 2: (j + 1) * 2,\ + ((i+1) * self.input_size) + j] = np.array([1., -1.]) + self.F = np.vstack((self.F, temp_F)) + + self.F1 = self.F[:, :self.input_size] + + temp_f = [] + for i in range(self.input_size): + temp_f.append(-1 * self.input_upper_bound[i]) + temp_f.append(self.input_lower_bound[i]) + + self.f = np.tile(np.array(temp_f).flatten(), self.pred_len) + + # about dt_input constraints + if self.dt_input_lower_bound is not None: + self.W = np.zeros((2, self.pred_len * self.input_size)) + self.W[:, 0] = np.array([1., -1.]) + + for i in range(self.pred_len * self.input_size - 1): + temp_W = np.zeros((2, self.pred_len * self.input_size)) + temp_W[:, i+1] = np.array([1., -1.]) + self.W = np.vstack((self.W, temp_W)) + + temp_omega = [] + + for i in range(self.input_size): + temp_omega.append(self.dt_input_upper_bound[i]) + temp_omega.append(-1. * self.dt_input_lower_bound[i]) + + self.omega = np.tile(np.array(temp_omega).flatten(), + self.pred_len) + + def obtain_sol(self, curr_x, g_xs): + """ calculate the optimal inputs + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + g_xs (numpy.ndarrya): goal trajectory, + shape(plan_len+1, state_size) + Returns: + opt_input (numpy.ndarray): optimal input, shape(input_size, ) + """ + temp_1 = np.matmul(self.phi_mat, curr_x.reshape(-1, 1)) + temp_2 = np.matmul(self.gamma_mat, self.history_u[-1].reshape(-1, 1)) + + error = g_xs[1:].reshape(-1, 1) - temp_1 - temp_2 + + G = np.matmul(self.theta_mat.T, np.matmul(self.Qs, error)) + + H = np.matmul(self.theta_mat.T, np.matmul(self.Qs, self.theta_mat)) \ + + self.Rs + H = H * 0.5 + + # constraints + A = [] + b = [] + + if self.W is not None: + A.append(self.W) + b.append(self.omega.reshape(-1, 1)) + + if self.F is not None: + b_F = - np.matmul(self.F1, self.history_u[-1].reshape(-1, 1)) \ + - self.f.reshape(-1, 1) + A.append(self.F) + b.append(b_F) + + A = np.array(A).reshape(-1, self.input_size * self.pred_len) + + ub = np.array(b).flatten() + + # make cvxpy problem formulation + P = 2*matrix(H) + q = matrix(-1 * G) + A = matrix(A) + b = matrix(ub) + + # solve the problem + opt_result = solvers.qp(P, q, G=A, h=b) + opt_dt_us = np.array(list(opt_result['x'])) + # to dt form + opt_dt_u_seq = np.cumsum(opt_dt_us.reshape(self.pred_len,\ + self.input_size), + axis=0) + + opt_u_seq = opt_dt_u_seq + self.history_u[-1] + + # save + self.history_u.append(opt_u_seq[0]) + + # check costs + costs = self.calc_cost(curr_x, + opt_u_seq.reshape(1, + self.pred_len, + self.input_size), + g_xs) + + logger.debug("Cost = {}".format(costs)) + + return opt_u_seq[0] + + def __str__(self): + return "LinearMPC" \ No newline at end of file diff --git a/PythonLinearNonlinearControl/controllers/mppi.py b/PythonLinearNonlinearControl/controllers/mppi.py new file mode 100644 index 0000000..fc8d887 --- /dev/null +++ b/PythonLinearNonlinearControl/controllers/mppi.py @@ -0,0 +1,124 @@ +from logging import getLogger + +import numpy as np +import scipy.stats as stats + +from .controller import Controller +from ..envs.cost import calc_cost + +logger = getLogger(__name__) + +class MPPI(Controller): + """ Model Predictive Path Integral for linear and nonlinear method + + Attributes: + history_u (list[numpy.ndarray]): time history of optimal input + Ref: + Nagabandi, A., Konoglie, K., Levine, S., & Kumar, V. (2019). + Deep Dynamics Models for Learning Dexterous Manipulation. + arXiv preprint arXiv:1909.11652. + """ + def __init__(self, config, model): + super(MPPI, self).__init__(config, model) + + # model + self.model = model + + # general parameters + self.pred_len = config.PRED_LEN + self.input_size = config.INPUT_SIZE + + # mppi parameters + self.beta = config.opt_config["MPPI"]["beta"] + self.pop_size = config.opt_config["MPPI"]["popsize"] + self.kappa = config.opt_config["MPPI"]["kappa"] + self.noise_sigma = config.opt_config["MPPI"]["noise_sigma"] + self.opt_dim = self.input_size * self.pred_len + + # get bound + self.input_upper_bounds = np.tile(config.INPUT_UPPER_BOUND, + (self.pred_len, 1)) + self.input_lower_bounds = np.tile(config.INPUT_LOWER_BOUND, + (self.pred_len, 1)) + + # get cost func + self.state_cost_fn = config.state_cost_fn + self.terminal_state_cost_fn = config.terminal_state_cost_fn + self.input_cost_fn = config.input_cost_fn + + # init mean + self.prev_sol = np.tile((config.INPUT_UPPER_BOUND \ + + config.INPUT_LOWER_BOUND) / 2., + self.pred_len) + self.prev_sol = self.prev_sol.reshape(self.pred_len, self.input_size) + + # save + self.history_u = [np.zeros(self.input_size)] + + def clear_sol(self): + """ clear prev sol + """ + logger.debug("Clear Solution") + self.prev_sol = \ + (self.input_upper_bounds + self.input_lower_bounds) / 2. + self.prev_sol = self.prev_sol.reshape(self.pred_len, self.input_size) + + def obtain_sol(self, curr_x, g_xs): + """ calculate the optimal inputs + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + g_xs (numpy.ndarrya): goal trajectory, shape(plan_len, state_size) + Returns: + opt_input (numpy.ndarray): optimal input, shape(input_size, ) + """ + # get noised inputs + noise = np.random.normal( + loc=0, scale=1.0, size=(self.pop_size, self.pred_len, + self.input_size)) * self.noise_sigma + noised_inputs = noise.copy() + + for t in range(self.pred_len): + if t > 0: + noised_inputs[:, t, :] = self.beta \ + * (self.prev_sol[t, :] \ + + noise[:, t, :]) \ + + (1 - self.beta) \ + * noised_inputs[:, t-1, :] + else: + noised_inputs[:, t, :] = self.beta \ + * (self.prev_sol[t, :] \ + + noise[:, t, :]) \ + + (1 - self.beta) \ + * self.history_u[-1] + + # clip actions + noised_inputs = np.clip( + noised_inputs, self.input_lower_bounds, self.input_upper_bounds) + + # calc cost + costs = self.calc_cost(curr_x, noised_inputs, g_xs) + rewards = -costs + + # mppi update + # normalize and get sum of reward + # exp_rewards.shape = (N, ) + exp_rewards = np.exp(self.kappa * (rewards - np.max(rewards))) + denom = np.sum(exp_rewards) + 1e-10 # avoid numeric error + + # weight actions + weighted_inputs = exp_rewards[:, np.newaxis, np.newaxis] \ + * noised_inputs + sol = np.sum(weighted_inputs, 0) / denom + + # update + self.prev_sol[:-1] = sol[1:] + self.prev_sol[-1] = sol[-1] # last use the terminal input + + # log + self.history_u.append(sol[0]) + + return sol[0] + + def __str__(self): + return "MPPI" \ No newline at end of file diff --git a/PythonLinearNonlinearControl/controllers/random.py b/PythonLinearNonlinearControl/controllers/random.py new file mode 100644 index 0000000..53a7622 --- /dev/null +++ b/PythonLinearNonlinearControl/controllers/random.py @@ -0,0 +1,77 @@ +from logging import getLogger + +import numpy as np +import scipy.stats as stats + +from .controller import Controller +from ..envs.cost import calc_cost + +logger = getLogger(__name__) + +class RandomShooting(Controller): + """ Random Shooting Method for linear and nonlinear method + + Attributes: + history_u (list[numpy.ndarray]): time history of optimal input + Ref: + Chua, K., Calandra, R., McAllister, R., & Levine, S. (2018). + Deep reinforcement learning in a handful of trials + using probabilistic dynamics models. + In Advances in Neural Information Processing Systems (pp. 4754-4765). + """ + def __init__(self, config, model): + super(RandomShooting, self).__init__(config, model) + + # model + self.model = model + + # general parameters + self.pred_len = config.PRED_LEN + self.input_size = config.INPUT_SIZE + + # cem parameters + self.pop_size = config.opt_config["Random"]["popsize"] + self.opt_dim = self.input_size * self.pred_len + + # get bound + self.input_upper_bounds = np.tile(config.INPUT_UPPER_BOUND, + self.pred_len) + self.input_lower_bounds = np.tile(config.INPUT_LOWER_BOUND, + self.pred_len) + + # get cost func + self.state_cost_fn = config.state_cost_fn + self.terminal_state_cost_fn = config.terminal_state_cost_fn + self.input_cost_fn = config.input_cost_fn + + # save + self.history_u = [] + + def obtain_sol(self, curr_x, g_xs): + """ calculate the optimal inputs + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + g_xs (numpy.ndarrya): goal trajectory, shape(plan_len, state_size) + Returns: + opt_input (numpy.ndarray): optimal input, shape(input_size, ) + """ + # set different seed + np.random.seed() + + samples = np.random.uniform(self.input_lower_bounds, + self.input_upper_bounds, + [self.pop_size, self.opt_dim]) + # calc cost + costs = self.calc_cost(curr_x, + samples.reshape(self.pop_size, + self.pred_len, + self.input_size), + g_xs) + # solution + sol = samples[np.argmin(costs)] + + return sol.reshape(self.pred_len, self.input_size).copy()[0] + + def __str__(self): + return "RandomShooting" \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/__init__.py b/PythonLinearNonlinearControl/envs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/envs/cost.py b/PythonLinearNonlinearControl/envs/cost.py new file mode 100644 index 0000000..5d10697 --- /dev/null +++ b/PythonLinearNonlinearControl/envs/cost.py @@ -0,0 +1,37 @@ +from logging import getLogger + +import numpy as np + +logger = getLogger(__name__) + +def calc_cost(pred_xs, input_sample, g_xs, + state_cost_fn, input_cost_fn, terminal_state_cost_fn): + """ calculate the cost + + Args: + pred_xs (numpy.ndarray): predicted state trajectory, + shape(pop_size, pred_len+1, state_size) + input_sample (numpy.ndarray): inputs samples trajectory, + shape(pop_size, pred_len+1, input_size) + g_xs (numpy.ndarray): goal state trajectory, + shape(pop_size, pred_len+1, state_size) + state_cost_fn (function): state cost fucntion + input_cost_fn (function): input cost fucntion + terminal_state_cost_fn (function): terminal state cost fucntion + Returns: + cost (numpy.ndarray): cost of the input sample, shape(pop_size, ) + """ + # state cost + state_pred_par_cost = state_cost_fn(pred_xs[:, 1:-1, :], g_xs[:, 1:-1, :]) + state_cost = np.sum(np.sum(state_pred_par_cost, axis=-1), axis=-1) + + # terminal cost + terminal_state_par_cost = terminal_state_cost_fn(pred_xs[:, -1, :], + g_xs[:, -1, :]) + terminal_state_cost = np.sum(terminal_state_par_cost, axis=-1) + + # act cost + act_pred_par_cost = input_cost_fn(input_sample) + act_cost = np.sum(np.sum(act_pred_par_cost, axis=-1), axis=-1) + + return state_cost + terminal_state_cost + act_cost \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/env.py b/PythonLinearNonlinearControl/envs/env.py new file mode 100644 index 0000000..c8ace4b --- /dev/null +++ b/PythonLinearNonlinearControl/envs/env.py @@ -0,0 +1,54 @@ +import numpy as np + +class Env(): + """ + Attributes: + curr_x (numpy.ndarray): current state + history_x (list[numpy.ndarray]): historty of state, shape(step_count*state_size) + step_count (int): step count + """ + def __init__(self, config): + """ + """ + self.config = config + self.curr_x = None + self.goal_state = None + self.history_x = [] + self.history_g_x = [] + self.step_count = None + + def reset(self, init_x=None): + """ reset state + Returns: + init_x (numpy.ndarray): initial state, shape(state_size, ) + info (dict): information + """ + self.step_count = 0 + + self.curr_x = np.zeros(self.config["state_size"]) + + if init_x is not None: + self.curr_x = init_x + + # clear memory + self.history_x = [] + self.history_g_x = [] + + return self.curr_x, {} + + def step(self, u): + """ + Args: + u (numpy.ndarray) : input, shape(input_size, ) + Returns: + next_x (numpy.ndarray): next state, shape(state_size, ) + cost (float): costs + done (bool): end the simulation or not + info (dict): information + """ + raise NotImplementedError("Implement step function") + + def __str__(self): + """ + """ + return self.config \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/first_order_lag.py b/PythonLinearNonlinearControl/envs/first_order_lag.py new file mode 100644 index 0000000..0348ee0 --- /dev/null +++ b/PythonLinearNonlinearControl/envs/first_order_lag.py @@ -0,0 +1,113 @@ +import numpy as np +import scipy +from scipy import integrate +from .env import Env + +class FirstOrderLagEnv(Env): + """ First Order Lag System Env + """ + def __init__(self, tau=0.63): + """ + """ + self.config = {"state_size" : 4,\ + "input_size" : 2,\ + "dt" : 0.05,\ + "max_step" : 500,\ + "input_lower_bound": [-0.5, -0.5],\ + "input_upper_bound": [0.5, 0.5], + } + + super(FirstOrderLagEnv, self).__init__(self.config) + + # to get discrete system matrix + self.A, self.B = self._to_state_space(tau, dt=self.config["dt"]) + + @staticmethod + def _to_state_space(tau, dt=0.05): + """ + Args: + tau (float): time constant + dt (float): discrte time + Returns: + A (numpy.ndarray): discrete A matrix + B (numpy.ndarray): discrete B matrix + """ + # continuous + Ac = np.array([[-1./tau, 0., 0., 0.], + [0., -1./tau, 0., 0.], + [1., 0., 0., 0.], + [0., 1., 0., 0.]]) + Bc = np.array([[1./tau, 0.], + [0., 1./tau], + [0., 0.], + [0., 0.]]) + # to discrete system + A = scipy.linalg.expm(dt*Ac) + # B = np.matmul(np.matmul(scipy.linalg.expm(Ac*dt) - + # scipy.linalg.expm(Ac*0.), np.linalg.inv(Ac)),\ + # Bc) + B = np.zeros_like(Bc) + for m in range(Bc.shape[0]): + for n in range(Bc.shape[1]): + integrate_fn =\ + lambda tau: np.matmul(scipy.linalg.expm(Ac*tau), Bc)[m, n] + sol = integrate.quad(integrate_fn, 0, dt) + B[m, n] = sol[0] + + return A, B + + def reset(self, init_x=None): + """ reset state + Returns: + init_x (numpy.ndarray): initial state, shape(state_size, ) + info (dict): information + """ + self.step_count = 0 + + self.curr_x = np.zeros(self.config["state_size"]) + + if init_x is not None: + self.curr_x = init_x + + # goal + self.goal_state = np.array([0., 0, -2., 3.]) + + # clear memory + self.history_x = [] + self.history_g_x = [] + + return self.curr_x, {"goal_state": self.goal_state} + + def step(self, u): + """ + Args: + u (numpy.ndarray) : input, shape(input_size, ) + Returns: + next_x (numpy.ndarray): next state, shape(state_size, ) + cost (float): costs + done (bool): end the simulation or not + info (dict): information + """ + # clip action + u = np.clip(u, + self.config["input_lower_bound"], + self.config["input_lower_bound"]) + + next_x = np.matmul(self.A, self.curr_x[:, np.newaxis]) \ + + np.matmul(self.B, u[:, np.newaxis]) + + # cost + cost = 0 + cost = np.sum(u**2) + cost += np.sum((self.curr_x-g_x)**2) + + # save history + self.history_x.append(next_x.flatten()) + self.history_g_x.append(self.goal_state.flatten()) + + # update + self.curr_x = next_x.flatten() + # update costs + self.step_count += 1 + + return next_x.flatten(), cost, self.step_count > self.config["max_step"], {"goal_state" : self.goal_state} \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/make_envs.py b/PythonLinearNonlinearControl/envs/make_envs.py new file mode 100644 index 0000000..aedd299 --- /dev/null +++ b/PythonLinearNonlinearControl/envs/make_envs.py @@ -0,0 +1,8 @@ +from .first_order_lag import FirstOrderLagEnv + +def make_env(args): + + if args.env == "FirstOrderLag": + return FirstOrderLagEnv() + + raise NotImplementedError("There is not {} Env".format(name)) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/two_wheeled.py b/PythonLinearNonlinearControl/envs/two_wheeled.py new file mode 100644 index 0000000..55f08f6 --- /dev/null +++ b/PythonLinearNonlinearControl/envs/two_wheeled.py @@ -0,0 +1,145 @@ +import numpy as np +import scipy +from scipy import integrate +from .env import Env + +class TwoWheeledConstEnv(Env): + """ Two wheeled robot with constant goal Env + """ + def __init__(self): + """ + """ + self.config = {"state_size" : 3,\ + "input_size" : 2,\ + "dt" : 0.01,\ + "max_step" : 500,\ + "input_lower_bound": [-1.5, -3.14],\ + "input_upper_bound": [1.5, 3.14], + } + + super(TwoWheeledEnv, self).__init__(self.config) + + def reset(self, init_x=None): + """ reset state + Returns: + init_x (numpy.ndarray): initial state, shape(state_size, ) + info (dict): information + """ + self.step_count = 0 + + self.curr_x = np.zeros(self.config["state_size"]) + + if init_x is not None: + self.curr_x = init_x + + # goal + self.goal_state = np.array([0., 0, -2., 3.]) + + # clear memory + self.history_x = [] + self.history_g_x = [] + + return self.curr_x, {"goal_state": self.goal_state} + + def step(self, u): + """ + Args: + u (numpy.ndarray) : input, shape(input_size, ) + Returns: + next_x (numpy.ndarray): next state, shape(state_size, ) + cost (float): costs + done (bool): end the simulation or not + info (dict): information + """ + # clip action + u = np.clip(u, + self.config["input_lower_bound"], + self.config["input_lower_bound"]) + + # step + next_x = np.matmul(self.A, self.curr_x[:, np.newaxis]) \ + + np.matmul(self.B, u[:, np.newaxis]) + + # TODO: implement costs + + # save history + self.history_x.append(next_x.flatten()) + self.history_g_x.append(self.goal_state.flatten()) + + # update + self.curr_x = next_x.flatten() + # update costs + self.step_count += 1 + + return next_x.flatten(), 0., self.step_count > self.config["max_step"], {"goal_state" : self.goal_state} + +class TwoWheeledEnv(Env): + """ Two wheeled robot Env + """ + def __init__(self): + """ + """ + self.config = {"state_size" : 3,\ + "input_size" : 2,\ + "dt" : 0.01,\ + "max_step" : 500,\ + "input_lower_bound": [-1.5, -3.14],\ + "input_upper_bound": [1.5, 3.14], + } + + super(TwoWheeledEnv, self).__init__(self.config) + + def reset(self, init_x=None): + """ reset state + Returns: + init_x (numpy.ndarray): initial state, shape(state_size, ) + info (dict): information + """ + self.step_count = 0 + + self.curr_x = np.zeros(self.config["state_size"]) + + if init_x is not None: + self.curr_x = init_x + + # goal + self.goal_state = np.array([0., 0, -2., 3.]) + + # clear memory + self.history_x = [] + self.history_g_x = [] + + return self.curr_x, {"goal_state": self.goal_state} + + def step(self, u): + """ + Args: + u (numpy.ndarray) : input, shape(input_size, ) + Returns: + next_x (numpy.ndarray): next state, shape(state_size, ) + cost (float): costs + done (bool): end the simulation or not + info (dict): information + """ + # clip action + u = np.clip(u, + self.config["input_lower_bound"], + self.config["input_lower_bound"]) + + # step + next_x = np.matmul(self.A, self.curr_x[:, np.newaxis]) \ + + np.matmul(self.B, u[:, np.newaxis]) + + # TODO: implement costs + + # save history + self.history_x.append(next_x.flatten()) + self.history_g_x.append(self.goal_state.flatten()) + + # update + self.curr_x = next_x.flatten() + # update costs + self.step_count += 1 + + return next_x.flatten(), 0., self.step_count > self.config["max_step"], {"goal_state" : self.goal_state} + diff --git a/PythonLinearNonlinearControl/helper.py b/PythonLinearNonlinearControl/helper.py new file mode 100644 index 0000000..7fa2058 --- /dev/null +++ b/PythonLinearNonlinearControl/helper.py @@ -0,0 +1,147 @@ +import argparse +import datetime +import json +import os +import sys +import six +import pickle +from logging import DEBUG, basicConfig, getLogger, FileHandler, StreamHandler, Formatter, Logger + +def make_logger(save_dir): + """ + Args: + save_dir (str): save directory + """ + # base config setting + basicConfig( + format='[%(asctime)s] %(name)s %(levelname)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + ) + + # mypackage log level + logger = getLogger("PythonLinearNonlinearControl") + logger.setLevel(DEBUG) + + # file handler + log_path = os.path.join(save_dir, "log.txt") + file_handler = FileHandler(log_path) + file_handler.setLevel(DEBUG) + file_handler.setFormatter(Formatter('%(message)s')) + logger.addHandler(file_handler) + + # sh handler + # sh_handler = StreamHandler() + # logger.addHandler(sh_handler) + +def int_tuple(s): + """ transform str to tuple + Args: + s (str): strings that you want to change + Returns: + tuple + """ + return tuple(int(i) for i in s.split(',')) + +def bool_flag(s): + """ transform str to bool flg + Args: + s (str): strings that you want to change + """ + if s == '1': + return True + elif s == '0': + return False + msg = 'Invalid value "%s" for bool flag (should be 0 or 1)' + raise ValueError(msg % s) + +def file_exists(path): + """ Check file existence on given path + Args: + path (str): path of the file to check existence + Returns: + file_existence (bool): True if file exists otherwise False + """ + return os.path.exists(path) + +def create_dir_if_not_exist(outdir): + """ Check directory existence and creates new directory if not exist + Args: + outdir (str): path of the file to create directory + RuntimeError: + file exists in outdir but it is not a directory + """ + if file_exists(outdir): + if not os.path.isdir(outdir): + raise RuntimeError('{} is not a directory'.format(outdir)) + else: + return + os.makedirs(outdir) + +def write_text_to_file(file_path, data): + """ Write given text data to file + Args: + file_path (str): path of the file to write data + data (str): text to write to the file + """ + with open(file_path, 'w') as f: + f.write(data) + +def read_text_from_file(file_path): + """ Read given file as text + Args: + file_path (str): path of the file to read data + Returns + data (str): text read from the file + """ + with open(file_path, 'r') as f: + return f.read() + +def save_pickle(file_path, data): + """ pickle given data to file + Args: + file_path (str): path of the file to pickle data + data (): data to pickle + """ + with open(file_path, 'wb') as f: + pickle.dump(data, f) + +def load_pickle(file_path): + """ load pickled data from file + Args: + file_path (str): path of the file to load pickled data + Returns: + data (): data pickled in file + """ + with open(file_path, 'rb') as f: + if six.PY2: + return pickle.load(f) + else: + return pickle.load(f, encoding='bytes') + +def prepare_output_dir(base_dir, args, time_format='%Y-%m-%d-%H%M%S'): + """ prepare a directory with current datetime as name. + created directory contains the command and args when the script was called as text file. + Args: + base_dir (str): path of the directory to save data + args (dict): arguments when the python script was called + time_format (str): datetime format string for naming directory to save data + Returns: + out_dir (str): directory to save data + """ + time_str = datetime.datetime.now().strftime(time_format) + outdir = os.path.join(base_dir, time_str) + + create_dir_if_not_exist(outdir) + + # Save all the arguments + args_file_path = os.path.join(outdir, 'args.txt') + if isinstance(args, argparse.Namespace): + args = vars(args) + write_text_to_file(args_file_path, json.dumps(args)) + + # Save the command + argv_file_path = os.path.join(outdir, 'command.txt') + argv = ' '.join(sys.argv) + write_text_to_file(argv_file_path, argv) + + return outdir \ No newline at end of file diff --git a/PythonLinearNonlinearControl/models/__init__.py b/PythonLinearNonlinearControl/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/models/first_order_lag.py b/PythonLinearNonlinearControl/models/first_order_lag.py new file mode 100644 index 0000000..a4a97fb --- /dev/null +++ b/PythonLinearNonlinearControl/models/first_order_lag.py @@ -0,0 +1,51 @@ +import numpy as np +import scipy.linalg +from scipy import integrate +from .model import LinearModel + +class FirstOrderLagModel(LinearModel): + """ first order lag model + Attributes: + curr_x (numpy.ndarray): + u (numpy.ndarray): + history_pred_xs (numpy.ndarray): + """ + def __init__(self, config, tau=0.63): + """ + Args: + tau (float): time constant + """ + # param + self.A, self.B = self._to_state_space(tau, dt=config.DT) # discrete system + super(FirstOrderLagModel, self).__init__(self.A, self.B) + + @staticmethod + def _to_state_space(tau, dt=0.05): + """ + Args: + tau (float): time constant + dt (float): discrte time + Returns: + A (numpy.ndarray): discrete A matrix + B (numpy.ndarray): discrete B matrix + """ + # continuous + Ac = np.array([[-1./tau, 0., 0., 0.], + [0., -1./tau, 0., 0.], + [1., 0., 0., 0.], + [0., 1., 0., 0.]]) + Bc = np.array([[1./tau, 0.], + [0., 1./tau], + [0., 0.], + [0., 0.]]) + # to discrete system + A = scipy.linalg.expm(dt*Ac) + # B = np.matmul(np.matmul(scipy.linalg.expm(Ac*dt)-scipy.linalg.expm(Ac*0.), np.linalg.inv(Ac)), Bc) + B = np.zeros_like(Bc) + for m in range(Bc.shape[0]): + for n in range(Bc.shape[1]): + integrate_fn = lambda tau: np.matmul(scipy.linalg.expm(Ac*tau), Bc)[m, n] + sol = integrate.quad(integrate_fn, 0, dt) + B[m, n] = sol[0] + + return A, B \ No newline at end of file diff --git a/PythonLinearNonlinearControl/models/make_models.py b/PythonLinearNonlinearControl/models/make_models.py new file mode 100644 index 0000000..73c3987 --- /dev/null +++ b/PythonLinearNonlinearControl/models/make_models.py @@ -0,0 +1,8 @@ +from .first_order_lag import FirstOrderLagModel + +def make_model(args, config): + + if args.env == "FirstOrderLag": + return FirstOrderLagModel(config) + + raise NotImplementedError("There is not {} Model".format(args.env)) diff --git a/PythonLinearNonlinearControl/models/model.py b/PythonLinearNonlinearControl/models/model.py new file mode 100644 index 0000000..27bbd34 --- /dev/null +++ b/PythonLinearNonlinearControl/models/model.py @@ -0,0 +1,190 @@ +import numpy as np + +class Model(): + """ base class of model + """ + def __init__(self): + """ + """ + pass + + def predict_traj(self, curr_x, us): + """ predict trajectories + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + us (numpy.ndarray): inputs, + shape(pred_len, input_size) + or shape(pop_size, pred_len, input_size) + Returns: + pred_xs (numpy.ndarray): predicted state, + shape(pred_len+1, state_size) including current state + or shape(pop_size, pred_len+1, state_size) + """ + if len(us.shape) == 3: + pred_xs =self._predict_traj_alltogether(curr_x, us) + elif len(us.shape) == 2: + pred_xs = self._predict_traj(curr_x, us) + else: + raise ValueError("Invalid us") + + return pred_xs + + def _predict_traj(self, curr_x, us): + """ predict trajectories + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + us (numpy.ndarray): inputs, shape(pred_len, input_size) + Returns: + pred_xs (numpy.ndarray): predicted state, + shape(pred_len+1, state_size) including current state + """ + # get size + pred_len = us.shape[0] + # initialze + x = curr_x + pred_xs = curr_x[np.newaxis, :] + + for t in range(pred_len): + next_x = self.predict_next_state(x, us[t]) + # update + pred_xs = np.concatenate((pred_xs, next_x[np.newaxis, :]), axis=0) + x = next_x + + return pred_xs + + def _predict_traj_alltogether(self, curr_x, us): + """ predict trajectories for all samples + + Args: + curr_x (numpy.ndarray): current state, shape(pop_size, state_size) + us (numpy.ndarray): inputs, shape(pop_size, pred_len, input_size) + Returns: + pred_xs (numpy.ndarray): predicted state, + shape(pop_size, pred_len+1, state_size) including current state + """ + # get size + (pop_size, pred_len, _) = us.shape + us = np.transpose(us, (1, 0, 2)) # to (pred_len, pop_size, input_size) + # initialze + x = np.tile(curr_x, (pop_size, 1)) + pred_xs = x[np.newaxis, :, :] # (1, pop_size, state_size) + + for t in range(pred_len): + # next_x.shape = (pop_size, state_size) + next_x = self.predict_next_state(x, us[t]) + # update + pred_xs = np.concatenate((pred_xs, next_x[np.newaxis, :, :]),\ + axis=0) + x = next_x + + return np.transpose(pred_xs, (1, 0, 2)) + + def predict_next_state(self, curr_x, u): + """ predict next state + """ + raise NotImplementedError("Implement the model") + + def predict_adjoint_traj(self, xs, us, g_xs): + """ + Args: + xs (numpy.ndarray): states trajectory, shape(pred_len+1, state_size) + us (numpy.ndarray): inputs, shape(pred_len, input_size) + g_xs (numpy.ndarray): goal states, shape(pred_len+1, state_size) + Returns: + lams (numpy.ndarray): adjoint state, shape(pred_len, state_size), + adjoint size is the same as state_size + """ + # get size + (pred_len, input_size) = us.shape + # pred final adjoint state + lam = self.predict_terminal_adjoint_state(xs[-1],\ + terminal_g_x=g_xs[-1]) + lams = lam[np.newaxis, :] + + for t in range(pred_len-1, 0, -1): + prev_lam = \ + self.predict_adjoint_state(lam, xs[t], us[t],\ + goal=g_xs[t], t=t) + # update + lams = np.concatenate((prev_lam[np.newaxis, :], lams), axis=0) + lam = prev_lam + + return lams + + def predict_adjoint_state(self, lam, x, u, goal=None, t=None): + """ predict adjoint states + + Args: + lam (numpy.ndarray): adjoint state, shape(state_size, ) + x (numpy.ndarray): state, shape(state_size, ) + u (numpy.ndarray): input, shape(input_size, ) + goal (numpy.ndarray): goal state, shape(state_size, ) + Returns: + prev_lam (numpy.ndarrya): previous adjoint state, + shape(state_size, ) + """ + raise NotImplementedError("Implement the adjoint model") + + def predict_terminal_adjoint_state(self, terminal_x, terminal_g_x=None): + """ predict terminal adjoint state + + Args: + terminal_x (numpy.ndarray): terminal state, shape(state_size, ) + terminal_g_x (numpy.ndarray): terminal goal state, + shape(state_size, ) + Returns: + terminal_lam (numpy.ndarray): terminal adjoint state, + shape(state_size, ) + """ + raise NotImplementedError("Implement terminal adjoint state") + + def gradient_x(self, x, u): + """ gradient of model with respect to the state + """ + raise NotImplementedError("Implement gradient of model \ + with respect to the state") + + def gradient_u(self, x, u): + """ gradient of model with respect to the input + """ + raise NotImplementedError("Implement gradient of model \ + with respect to the input") + +class LinearModel(Model): + """ discrete linear model, x[k+1] = Ax[k] + Bu[k] + + Attributes: + A (numpy.ndarray): shape(state_size, state_size) + B (numpy.ndarray): shape(state_size, input_size) + """ + def __init__(self, A, B): + """ + """ + super(LinearModel, self).__init__() + self.A = A + self.B = B + + def predict_next_state(self, curr_x, u): + """ predict next state + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) or + shape(pop_size, state_size) + u (numpy.ndarray): input, shape(input_size, ) or + shape(pop_size, input_size) + Returns: + next_x (numpy.ndarray): next state, shape(state_size, ) or + shape(pop_size, state_size) + """ + if len(u.shape) == 1: + next_x = np.matmul(self.A, curr_x[:, np.newaxis]) \ + + np.matmul(self.B, u[:, np.newaxis]) + + return next_x.flatten() + + elif len(u.shape) == 2: + next_x = np.matmul(curr_x, self.A.T) + np.matmul(u, self.B.T) + + return next_x diff --git a/PythonLinearNonlinearControl/planners/__init__.py b/PythonLinearNonlinearControl/planners/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/planners/const_planner.py b/PythonLinearNonlinearControl/planners/const_planner.py new file mode 100644 index 0000000..5806c62 --- /dev/null +++ b/PythonLinearNonlinearControl/planners/const_planner.py @@ -0,0 +1,23 @@ +import numpy as np +from .planner import Planner + +class ConstantPlanner(): + """ This planner make constant goal state + """ + def __init__(self, config): + """ + """ + super(ConstantPlanner, self).__init__() + self.pred_len = config.PRED_LEN + self.state_size = config.STATE_SIZE + + def plan(self, curr_x, g_x=None): + """ + Args: + curr_x (numpy.ndarray): current state, shape(state_size) + g_x (numpy.ndarray): goal state, shape(state_size), + this state should be obtained from env + Returns: + g_xs (numpy.ndarrya): goal state, shape(pred_len, state_size) + """ + return np.tile(g_x, (self.pred_len+1, 1)) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/planners/make_planners.py b/PythonLinearNonlinearControl/planners/make_planners.py new file mode 100644 index 0000000..9da7f31 --- /dev/null +++ b/PythonLinearNonlinearControl/planners/make_planners.py @@ -0,0 +1,8 @@ +from .const_planner import ConstantPlanner + +def make_planner(args, config): + + if args.planner_type == "const": + return ConstantPlanner(config) + + raise NotImplementedError("There is not {} Planner".format(args.planner_type)) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/planners/planner.py b/PythonLinearNonlinearControl/planners/planner.py new file mode 100644 index 0000000..7e20b4f --- /dev/null +++ b/PythonLinearNonlinearControl/planners/planner.py @@ -0,0 +1,18 @@ +import numpy as np + +class Planner(): + """ + """ + def __init__(self): + """ + """ + pass + + def plan(self, curr_x): + """ + Args: + curr_x (numpy.ndarray): current state, shape(state_size) + Returns: + g_xs (numpy.ndarrya): goal state, shape(pred_len, state_size) + """ + raise NotImplementedError("Implement plan func") \ No newline at end of file diff --git a/PythonLinearNonlinearControl/plotters/__init__.py b/PythonLinearNonlinearControl/plotters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/plotters/plot_func.py b/PythonLinearNonlinearControl/plotters/plot_func.py new file mode 100644 index 0000000..216788e --- /dev/null +++ b/PythonLinearNonlinearControl/plotters/plot_func.py @@ -0,0 +1,60 @@ +import os + +import numpy as np +import matplotlib.pyplot as plt + +def plot_result(history, history_g=None, ylabel="x", + save_dir="./result", name="state_history"): + """ + Args: + history (numpy.ndarray): history, shape(iters, size) + """ + (iters, size) = history.shape + for i in range(0, size, 3): + + figure = plt.figure() + axis1 = figure.add_subplot(311) + axis2 = figure.add_subplot(312) + axis3 = figure.add_subplot(313) + + axis1.set_ylabel(ylabel + "_{}".format(i)) + axis2.set_ylabel(ylabel + "_{}".format(i+1)) + axis3.set_ylabel(ylabel + "_{}".format(i+2)) + axis3.set_xlabel("time steps") + + # gt + def plot(axis, history, history_g=None): + axis.plot(range(iters), history, c="r", linewidth=3) + if history_g is not None: + axis.plot(range(iters), history_g,\ + c="b", linewidth=3, label="goal") + + if i < size: + plot(axis1, history[:, i], history_g=history_g[:, i]) + if i+1 < size: + plot(axis2, history[:, i+1], history_g=history_g[:, i+1]) + if i+2 < size: + plot(axis3, history[:, i+2], history_g=history_g[:, i+2]) + + # save + if save_dir is not None: + path = os.path.join(save_dir, name + "-{}".format(i)) + else: + path = name + + axis1.legend(ncol=1, bbox_to_anchor=(0., 1.02, 1., 0.102), loc=3) + figure.savefig(path, bbox_inches="tight", pad_inches=0.05) + +def plot_results(args, history_x, history_u, history_g=None): + """ + Args: + history_x (numpy.ndarray): history of state, shape(iters, state_size) + history_u (numpy.ndarray): history of state, shape(iters, input_size) + Returns: + """ + plot_result(history_x, history_g=history_g, ylabel="x", + name="state_history", + save_dir="./result/" + args.controller_type) + plot_result(history_u, history_g=np.zeros_like(history_u), ylabel="u", + name="input_history", + save_dir="./result/" + args.controller_type) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/runners/__init__.py b/PythonLinearNonlinearControl/runners/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PythonLinearNonlinearControl/runners/make_runners.py b/PythonLinearNonlinearControl/runners/make_runners.py new file mode 100644 index 0000000..be08186 --- /dev/null +++ b/PythonLinearNonlinearControl/runners/make_runners.py @@ -0,0 +1,4 @@ +from .runner import ExpRunner + +def make_runner(args): + return ExpRunner() \ No newline at end of file diff --git a/PythonLinearNonlinearControl/runners/runner.py b/PythonLinearNonlinearControl/runners/runner.py new file mode 100644 index 0000000..0c55bb9 --- /dev/null +++ b/PythonLinearNonlinearControl/runners/runner.py @@ -0,0 +1,51 @@ +from logging import getLogger + +import numpy as np + +logger = getLogger(__name__) + +class ExpRunner(): + """ experiment runner + """ + def __init__(self): + """ + """ + pass + + def run(self, env, controller, planner): + """ + Returns: + history_x (numpy.ndarray): history of the state, + shape(episode length, state_size) + history_u (numpy.ndarray): history of the state, + shape(episode length, input_size) + """ + done = False + curr_x, info = env.reset() + history_x, history_u, history_g = [], [], [] + step_count = 0 + score = 0. + + while not done: + logger.debug("Step = {}".format(step_count)) + # plan + g_xs = planner.plan(curr_x, g_x=info["goal_state"]) + + # obtain sol + u = controller.obtain_sol(curr_x, g_xs) + + # step + next_x, cost, done, info = env.step(u) + + # save + history_u.append(u) + history_x.append(curr_x) + history_g.append(g_xs[0]) + # update + curr_x = next_x + score += cost + step_count += 1 + + logger.debug("Controller type = {}, Score = {}"\ + .format(controller, score)) + return np.array(history_x), np.array(history_u), np.array(history_g) \ No newline at end of file diff --git a/README.md b/README.md index ade05e1..e4381d4 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,148 @@ [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -# linear_nonlinear_control -Implementing the linear and nonlinear control theories. -For an instance, model predictive control, nonlinear model predictive control, sliding mode control etc +# PythonLinearNonLinearControl -Now you can see the examples of control theories as following +PythonLinearNonLinearControl is a library implementing the linear and nonlinear control theories in python. -- **Iterative Linear Quadratic Regulator (iLQR)** -- **Nonlinear Model Predictive Control (NMPC) with CGMRES** -- **Nonlinear Model Predictive Control (NMPC) with Newton method** -- **Linear Model Predictive Control (MPC)**(as generic function such as matlab tool box) -- **Extended Linear Model Predictive Control for vehicle model** +# Algorithms -# Usage and Details -you can see the usage in each folder +| Algorithm | Use Linear Model | Use Nonlinear Model | Need Gradient (Hamiltonian) | Need Gradient (Model) | +|:----------|:---------------:|:----------------:|:----------------:| +| Linear Model Predictive Control (MPC) | ✓ | x | x | x | +| Cross Entropy Method (CEM) | ✓ | ✓ | x | x | +| Model Preidictive Path Integral Control (MPPI) | ✓ | ✓ | x | x | +| Random Shooting Method (Random) | ✓ | ✓ | x | x | +| Iterative LQR (iLQR) | ✓ | ✓ | x | ✓ | +| Nonlinear Model Predictive Control -CGMRES- (NMPC-CGMRES) | ✓ | ✓ | ✓ | x | +| Nonlinear Model Predictive Control -Newton- (NMPC-Newton) | ✓ | ✓ | x | x | -# Basic Requirement +"Need Gradient" means that you have to implement the gradient of the model or the gradient of hamiltonian. +This library is also easily to extend for your own situations. + +Following algorithms are implemented in PythonLinearNonlinearControl + +- [Linear Model Predictive Control (MPC)](http://www2.eng.cam.ac.uk/~jmm1/mpcbook/mpcbook.html) + - Ref: Maciejowski, J. M. (2002). Predictive control: with constraints. + - [script]() +- [Cross Entropy Method (CEM)](https://arxiv.org/abs/1805.12114) + - Ref: Chua, K., Calandra, R., McAllister, R., & Levine, S. (2018). Deep reinforcement learning in a handful of trials using probabilistic dynamics models. In Advances in Neural Information Processing Systems (pp. 4754-4765) + - [script]() +- [Model Preidictive Path Integral Control (MPPI)](https://arxiv.org/abs/1909.11652) + - Ref: Nagabandi, A., Konoglie, K., Levine, S., & Kumar, V. (2019). Deep Dynamics Models for Learning Dexterous Manipulation. arXiv preprint arXiv:1909.11652. + - [script]() +- [Random Shooting Method (Random)](https://arxiv.org/abs/1805.12114) + - Ref: Chua, K., Calandra, R., McAllister, R., & Levine, S. (2018). Deep reinforcement learning in a handful of trials using probabilistic dynamics models. In Advances in Neural Information Processing Systems (pp. 4754-4765) + - [script]() +- [Iterative LQR (iLQR)](https://ieeexplore.ieee.org/document/6386025) + - Ref: Tassa, Y., Erez, T., & Todorov, E. (2012, October). Synthesis and stabilization of complex behaviors through online trajectory optimization. In 2012 IEEE/RSJ International Conference on Intelligent Robots and Systems (pp. 4906-4913). IEEE. and [Study Wolf](https://github.com/studywolf/control) + - [script]() +- [Unconstrained Nonlinear Model Predictive Control](https://www.sciencedirect.com/science/article/pii/S0005109897000058) + - Ref: Ohtsuka, T., & Fujii, H. A. (1997). Real-time optimization algorithm for nonlinear receding-horizon control. Automatica, 33(6), 1147-1154. + - [script]() +- [Constrained Nonlinear Model Predictive Control -CGMRES- (NMPC-CGMRES)](https://www.sciencedirect.com/science/article/pii/S0005109897000058) + - Ref: Ohtsuka, T., & Fujii, H. A. (1997). Real-time optimization algorithm for nonlinear receding-horizon control. Automatica, 33(6), 1147-1154. + - [script]() +- [Constrained Nonlinear Model Predictive Control -Newton- (NMPC-Newton)](https://www.sciencedirect.com/science/article/pii/S0005109897000058) + - Ref: Ohtsuka, T., & Fujii, H. A. (1997). Real-time optimization algorithm for nonlinear receding-horizon control. Automatica, 33(6), 1147-1154. + - [script]() + +# Environments + +| Name | Linear | Nonlinear | State Size | Input size | +|:----------|:---------------:|:----------------:|:----------------:| +| First Order Lag System | ✓ | x | 4 | 2 | +| Auto Cruse Control System | x | ✓ | 4 | 2 | +| Two wheeled System | x | ✓ | 4 | 2 | + +All environments are continuous. +**It should be noted that the algorithms for linear model could be applied to nonlinear enviroments if you have linealized the model of nonlinear environments.** + +# Usage + +## To install this package + +``` +python setup.py install +``` + +or + +``` +pip install . +``` + +## When developing the package + +``` +python setup.py develop +``` + +or + +``` +pip install -e . +``` + +## Run Experiments + +You can run the experiments as follows: + +``` +python scripts/simple_run.py --model "first-order_lag" --controller "CEM" +``` + +**figures and animations are saved in the ./result folder.** + +# Basic concepts + +When we design control systems, we should have **Model**, **Planner**, **Controller** and **Runner** as shown in the figure. +It should be noted that **Model** and **Environment** are different. As mentioned before, we the algorithms for linear model could be applied to nonlinear enviroments if you have linealized model of nonlinear environments. In addition, you can use Neural Network or any non-linear functions to the model, although this library can not deal with it now. + +## Model + +System model. For an instance, in the case that a model is linear, this model should have a form, "x[k+1] = Ax[k] + Bu[k]". + +If you use gradient based control method, you are preferred to implement the gradients of the model, other wise the controllers use numeric gradients. + +## Planner + +Planner make the goal states. + +## Controller + +Controller calculate the optimal inputs by using the model by using the algorithms. + +## Runner + +Runner runs the simulation. + +Please, see more detail in each scripts. + +# Old version + +If you are interested in the old version of this library, that was not a library just examples, please see [v1.0](https://github.com/Shunichi09/PythonLinearNonlinearControl/tree/v1.0) + +# Documents + +Coming soon !! + +# Requirements -- python3.5 or more - numpy - matplotlib +- cvxopt +- scipy # License -MIT + +[MIT License](LICENSE). # Citation ``` -@Misc{pythonlinearnonlinearControl, +@Misc{PythonLinearNonLinearControl, author = {Shunichi Sekiguchi}, -title = {pythonlinearnonlinearControl}, -note = "\url{https://github.com/Shunichi09/linear_nonlinear_control}", +title = {PythonLinearNonlinearControl}, +note = "\url{https://github.com/Shunichi09/PythonLinearNonlinearControl}", } ``` diff --git a/ilqr/README.md b/ilqr/README.md deleted file mode 100644 index 73a1c88..0000000 --- a/ilqr/README.md +++ /dev/null @@ -1,76 +0,0 @@ -# Iterative Linear Quadratic Regulator -This program is about iLQR (Iteratice Linear Quadratic Regulator) - -# Problem Formulation - -Two wheeled robot is expressed by the following equation. -It is nonlinear and nonholonomic system. Sometimes, it's extremely difficult to control the -steering(or angular velocity) and velocity of the wheeled robot. Therefore, many methods control only steering, like purepersuit, Linear MPC. -However, sometimes we should consider the velocity and steering simultaneously when the car or robots move fast. -To solve the problem, we should apply the control methods which can treat the nonlinear system. - - - -Nonliner Model Predictive Control is one of the famous methods, so I applied the method to two-wheeled robot which is included in the folder of this repository. -(if you are interested, please go to nmpc/ folder of this repository) - -NMPC is very effecitive method to solve nonlinear optimal control problem but it is a handcraft method. -This program is about one more other methods to solve the nonlinear optimal control problem. - -The method is iterative LQR. -Iterative LQR is one of the DDP(differential dynamic programming) methods. -Recently, this method is used in model-based RL(reinforcement learning). -Although, this method cannot guarantee to obtain the global optimal answer, we could apply any model such as nonliner model or time-varing model even the model that expressed by NN. -(Still we can only get approximate optimal anwser) - -If you want to know more about the iLQR, please look the references. -The paper and website are great. - -# Usage - -## static goal - -``` -$ python3 main_static.py -``` - -## dynamic goal - -``` -$ python3 main_dynamic.py -``` - -# Expected Results - -- static goal - - - - - - -- track the goal - - - - - - - -# Requirement - -- python3.5 or more -- numpy -- matplotlib - -# Reference - -- study wolf -https://github.com/studywolf/control - -- Sergey Levine's lecture -http://rail.eecs.berkeley.edu/deeprlcourse/ - -- Tassa, Y., Erez, T., & Todorov, E. (2012). Synthesis and stabilization of complex behaviors through online trajectory optimization. IEEE International Conference on Intelligent Robots and Systems, 4906–4913. https://doi.org/10.1109/IROS.2012.6386025 - -- Li, W., & Todorov, E. (n.d.). Iterative Linear Quadratic Regulator Design for Nonlinear Biological Movement Systems. Retrieved from https://homes.cs.washington.edu/~todorov/papers/LiICINCO04.pdf diff --git a/ilqr/animation.py b/ilqr/animation.py deleted file mode 100644 index ccb3b0d..0000000 --- a/ilqr/animation.py +++ /dev/null @@ -1,297 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.animation as ani -import matplotlib.font_manager as fon -import sys -import math - -# default setting of figures -plt.rcParams["mathtext.fontset"] = 'stix' # math fonts -plt.rcParams['xtick.direction'] = 'in' # x axis in -plt.rcParams['ytick.direction'] = 'in' # y axis in -plt.rcParams["font.size"] = 10 -plt.rcParams['axes.linewidth'] = 1.0 # axis line width -plt.rcParams['axes.grid'] = True # make grid - -def coordinate_transformation_in_angle(positions, base_angle): - ''' - Transformation the coordinate in the angle - - Parameters - ------- - positions : numpy.ndarray - this parameter is composed of xs, ys - should have (2, N) shape - base_angle : float [rad] - - Returns - ------- - traslated_positions : numpy.ndarray - the shape is (2, N) - - ''' - if positions.shape[0] != 2: - raise ValueError('the input data should have (2, N)') - - positions = np.array(positions) - positions = positions.reshape(2, -1) - - rot_matrix = [[np.cos(base_angle), np.sin(base_angle)], - [-1*np.sin(base_angle), np.cos(base_angle)]] - - rot_matrix = np.array(rot_matrix) - - translated_positions = np.dot(rot_matrix, positions) - - return translated_positions - -def square_make_with_angles(center_x, center_y, size, angle): - ''' - Create square matrix with angle line matrix(2D) - - Parameters - ------- - center_x : float in meters - the center x position of the square - center_y : float in meters - the center y position of the square - size : float in meters - the square's half-size - angle : float in radians - - Returns - ------- - square xs : numpy.ndarray - lenght is 5 (counterclockwise from right-up) - square ys : numpy.ndarray - length is 5 (counterclockwise from right-up) - angle line xs : numpy.ndarray - angle line ys : numpy.ndarray - ''' - - # start with the up right points - # create point in counterclockwise - square_xys = np.array([[size, 0.5 * size], [-size, 0.5 * size], [-size, -0.5 * size], [size, -0.5 * size], [size, 0.5 * size]]) - trans_points = coordinate_transformation_in_angle(square_xys.T, -angle) # this is inverse type - trans_points += np.array([[center_x], [center_y]]) - - square_xs = trans_points[0, :] - square_ys = trans_points[1, :] - - angle_line_xs = [center_x, center_x + math.cos(angle) * size] - angle_line_ys = [center_y, center_y + math.sin(angle) * size] - - return square_xs, square_ys, np.array(angle_line_xs), np.array(angle_line_ys) - - -def circle_make_with_angles(center_x, center_y, radius, angle): - ''' - Create circle matrix with angle line matrix - - Parameters - ------- - center_x : float - the center x position of the circle - center_y : float - the center y position of the circle - radius : float - angle : float [rad] - - Returns - ------- - circle xs : numpy.ndarray - circle ys : numpy.ndarray - angle line xs : numpy.ndarray - angle line ys : numpy.ndarray - ''' - - point_num = 100 # 分解能 - - circle_xs = [] - circle_ys = [] - - for i in range(point_num + 1): - circle_xs.append(center_x + radius * math.cos(i*2*math.pi/point_num)) - circle_ys.append(center_y + radius * math.sin(i*2*math.pi/point_num)) - - angle_line_xs = [center_x, center_x + math.cos(angle) * radius] - angle_line_ys = [center_y, center_y + math.sin(angle) * radius] - - return np.array(circle_xs), np.array(circle_ys), np.array(angle_line_xs), np.array(angle_line_ys) - - -class AnimDrawer(): - """create animation of path and robot - - Attributes - ------------ - cars : - anim_fig : figure of matplotlib - axis : axis of matplotlib - - """ - def __init__(self, objects): - """ - Parameters - ------------ - objects : list of objects - - Notes - --------- - lead_history_states, lead_history_predict_states, traj_ref, history_traj_ref, history_angle_ref - """ - self.car_history_state = objects[0] - self.target = objects[1] - - self.history_target_x = [self.target[:, 0]] - self.history_target_y = [self.target[:, 1]] - - self.history_xs = [self.car_history_state[:, 0]] - self.history_ys = [self.car_history_state[:, 1]] - self.history_ths = [self.car_history_state[:, 2]] - - # setting up figure - self.anim_fig = plt.figure() - self.axis = self.anim_fig.add_subplot(111) - - # imgs - self.car_imgs = [] - self.traj_imgs = [] - - def draw_anim(self, interval=10): - """draw the animation and save - - Parameteres - ------------- - interval : int, optional - animation's interval time, you should link the sampling time of systems - default is 50 [ms] - """ - self._set_axis() - self._set_img() - - self.skip_num = 2 - frame_num = int((len(self.history_xs[0])-1) / self.skip_num) - - animation = ani.FuncAnimation(self.anim_fig, self._update_anim, interval=interval, frames=frame_num) - - # self.axis.legend() - print('save_animation?') - shuold_save_animation = int(input()) - - if shuold_save_animation: - print('animation_number?') - num = int(input()) - animation.save('animation_{0}.mp4'.format(num), writer='ffmpeg') - # animation.save("Sample.gif", writer = 'imagemagick') # gif保存 - - plt.show() - - def _set_axis(self): - """ initialize the animation axies - """ - # (1) set the axis name - self.axis.set_xlabel(r'$\it{x}$ [m]') - self.axis.set_ylabel(r'$\it{y}$ [m]') - self.axis.set_aspect('equal', adjustable='box') - - LOW_MARGIN = 2 - HIGH_MARGIN = 2 - - self.axis.set_xlim(np.min(self.history_xs) - LOW_MARGIN, np.max(self.history_xs) + HIGH_MARGIN) - self.axis.set_ylim(np.min(self.history_ys) - LOW_MARGIN, np.max(self.history_ys) + HIGH_MARGIN) - - def _set_img(self): - """ initialize the imgs of animation - this private function execute the make initial imgs for animation - """ - # object imgs - obj_color_list = ["k", "k", "m", "m"] - obj_styles = ["solid", "solid", "solid", "solid"] - - for i in range(len(obj_color_list)): - temp_img, = self.axis.plot([], [], color=obj_color_list[i], linestyle=obj_styles[i]) - self.car_imgs.append(temp_img) - - traj_color_list = ["k", "b"] - - for i in range(len(traj_color_list)): - temp_img, = self.axis.plot([],[], color=traj_color_list[i], linestyle="dashed") - self.traj_imgs.append(temp_img) - - temp_img, = self.axis.plot([],[], "*", color="b") - self.traj_imgs.append(temp_img) - - def _update_anim(self, i): - """the update animation - this function should be used in the animation functions - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - - Returns - ----------- - object_imgs : list of img - traj_imgs : list of img - """ - i = int(i * self.skip_num) - - # self._draw_set_axis(i) - self._draw_car(i) - self._draw_traj(i) - # self._draw_prediction(i) - - return self.car_imgs, self.traj_imgs, - - def _draw_set_axis(self, i): - """ - """ - # (2) set the xlim and ylim - LOW_MARGIN = 20 - HIGH_MARGIN = 20 - OVER_LOOK = 50 - self.axis.set_xlim(np.min(self.history_xs[0][i : i + OVER_LOOK]) - LOW_MARGIN, np.max(self.history_xs[0][i : i + OVER_LOOK]) + HIGH_MARGIN) - self.axis.set_ylim(np.min(self.history_ys[0][i : i + OVER_LOOK]) - LOW_MARGIN, np.max(self.history_ys[0][i : i + OVER_LOOK]) + HIGH_MARGIN) - - def _draw_car(self, i): - """ - This private function is just divided thing of - the _update_anim to see the code more clear - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - """ - # cars - object_x, object_y, angle_x, angle_y = square_make_with_angles(self.history_xs[0][i], - self.history_ys[0][i], - 1.0, - self.history_ths[0][i]) - - self.car_imgs[0].set_data([object_x, object_y]) - self.car_imgs[1].set_data([angle_x, angle_y]) - - def _draw_traj(self, i): - """ - This private function is just divided thing of - the _update_anim to see the code more clear - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - """ - # car - self.traj_imgs[0].set_data(self.history_xs[0][:i], self.history_ys[0][:i]) - - # goal - self.traj_imgs[-1].set_data(self.history_target_x[0][i-1], self.history_target_y[0][i-1]) - - # traj_ref - self.traj_imgs[1].set_data(self.history_target_x[0][:i], self.history_target_y[0][:i]) \ No newline at end of file diff --git a/ilqr/goal_maker.py b/ilqr/goal_maker.py deleted file mode 100644 index 2112006..0000000 --- a/ilqr/goal_maker.py +++ /dev/null @@ -1,117 +0,0 @@ -import numpy as np -import math -import matplotlib.pyplot as plt - -def make_trajectory(goal_type, N): - """ - Parameters - ------------- - goal_type : str - goal type - N : int - length of reference trajectory - Returns - ----------- - ref_trajectory : numpy.ndarray, shape(N, STATE_SIZE) - - Notes - --------- - this function can only deal with the - """ - - if goal_type == "const": - ref_trajectory = np.array([[5., 3., 0.]]) - - return ref_trajectory - - if goal_type == "sin": - # parameters - amplitude = 2. - num_period = 2. - - ref_x_trajectory = np.linspace(0., 2 * math.pi * num_period, N) - ref_y_trajectory = amplitude * np.sin(ref_x_trajectory) - - ref_xy_trajectory = np.stack((ref_x_trajectory, ref_y_trajectory)) - - # target of theta is always zero - ref_trajectory = np.vstack((ref_xy_trajectory, np.zeros((1, N)))) - - return ref_trajectory.T - -class GoalMaker(): - """ - Attributes - ----------- - N : int - length of reference - goal_type : str - goal type - dt : float - sampling time - ref_traj : numpy.ndarray, shape(N, STATE_SIZE) - in this case the state size is 3 - """ - - def __init__(self, N=500, goal_type="const", dt=0.01): - """ - Parameters - -------------- - N : int - length of reference - goal_type : str - goal type - dt : float - sampling time - """ - self.N = N - self.goal_type = goal_type - self.dt = dt - - self.ref_traj = None - self.history_target = [] - - def preprocess(self): - """preprocess of make goal - """ - goal_type_list = ["const", "sin"] - - if self.goal_type not in goal_type_list: - raise KeyError("{0} is not in implemented goal type. please implement that!!".format(self.goal_type)) - - self.ref_traj = make_trajectory(self.goal_type, self.N) - - def calc_goal(self, x): - """ calclate nearest goal - Parameters - ------------ - x : numpy.ndarray, shape(STATE_SIZE, ) - state of the system - - Returns - ------------ - goal : numpy.ndarray, shape(STATE_SIZE, ) - """ - - # search nearest point - x_dis = (self.ref_traj[:, 0]-x[0])**2 - y_dis = (self.ref_traj[:, 1]-x[1])**2 - index = np.argmin(np.sqrt(x_dis + y_dis)) - - print(index) - - MARGIN = 30 - if not self.goal_type == "const": - index += MARGIN - - if index > self.N-1: - index = self.N - 1 - - goal = self.ref_traj[index] - - self.history_target.append(goal) - - return goal - -if __name__ == "__main__": - make_trajectory("sin", 400) \ No newline at end of file diff --git a/ilqr/ilqr.py b/ilqr/ilqr.py deleted file mode 100644 index 21a0c16..0000000 --- a/ilqr/ilqr.py +++ /dev/null @@ -1,463 +0,0 @@ -import numpy as np -from copy import copy, deepcopy - -from model import TwoWheeledCar - -class iLQRController(): - """ - A controller that implements iterative Linear Quadratic control. - Controls the (x, y, th) of the two wheeled car - - Attributes: - ------------ - tN : int - number of time step - STATE_SIZE : int - system state size - INPUT_SIZE : int - system input size - dt : float - sampling time - max_iter : int - number of max iteration - lamb_factor : int - lambda factor is that the adding value to make the matrix of Q _uu be positive - lamb_max : float - maximum lambda value to make the matrix of Q _uu be positive - eps_converge : float - threshold value of the iteration - """ - - def __init__(self, N=100, max_iter=400, dt=0.01): - """ - Parameters - ---------- - N : int, optional - number of time step, default is 100 - max_iter : int, optional - number of max iteration, default is 400 - dt : float, optional - sampling time, default is 0.01 - """ - self.tN = N - self.STATE_SIZE = 3 - self.INPUT_SIZE = 2 - self.dt = dt - - self.max_iter = max_iter - self.lamb_factor = 10 - self.lamb_max = 1e4 - self.eps_converge = 0.001 - - def calc_input(self, car, x_target): - """main loop of iterative LQR - - Parameters - ------------- - car : model class - should have initialize state and update state - x_target : numpy.ndarray, shape(STATE_SIZE, ) - target state - - Returns - ----------- - u : numpy.ndarray, shape(INPUT_SIZE, ) - - See also - ---------- - model.py - """ - - # initialize - self.reset(x_target) - - # Compute the optimization - x0 = np.zeros(self.STATE_SIZE) - self.simulator, x0 = self.initialize_simulator(car) - U = np.copy(self.U) - self.X, self.U, cost = self.ilqr(x0, U) - - self.u = self.U[self.t] # use only one time step (like MPC) - - return self.u - - def initialize_simulator(self, car): - """ make copy for controller - Parameters - ------------- - car : model class - should have initialize state and update state - - Returns - ---------- - simulator : model class - should have initialize state and update state - x0 : numpy.ndarray, shape(STATE_SIZE) - initial state of the simulator - """ - # copy - simulator = TwoWheeledCar(deepcopy(car.xs)) - - return simulator, deepcopy(simulator.xs) - - def cost(self, xs, us): - """ the immediate state cost function - - Parameters - ------------ - xs : shape(STATE_SIZE, tN + 1) - predict state of the system - us : shape(STATE_SIZE, tN) - predict input of the system - - Returns - ---------- - l : float - stage cost - l_x : numpy.ndarray, shape(STATE_SIZE, ) - differential of stage cost by x - l_xx : numpy.ndarray, shape(STATE_SIZE, STATE_SIZE) - second order differential of stage cost by x - l_u : numpy.ndarray, shape(INPUT_SIZE, ) - differential of stage cost by u - l_uu : numpy.ndarray, shape(INPUT_SIZE, INPUT_SIZE) - second order differential of stage cost by uu - l_ux numpy.ndarray, shape(INPUT_SIZE, STATE_SIZE) - second order differential of stage cost by ux - """ - - # total cost - R_11 = 1e-4 # terminal u_v cost weight - R_22 = 1e-4 # terminal u_th cost weight - - l = np.dot(us.T, np.dot(np.diag([R_11, R_22]), us)) - - # compute derivatives of cost - l_x = np.zeros(self.STATE_SIZE) - l_xx = np.zeros((self.STATE_SIZE, self.STATE_SIZE)) - - l_u1 = 2. * us[0] * R_11 - l_u2 = 2. * us[1] * R_22 - - l_u = np.array([l_u1, l_u2]) - - l_uu = 2. * np.diag([R_11, R_22]) - - l_ux = np.zeros((self.INPUT_SIZE, self.STATE_SIZE)) - - return l, l_x, l_xx, l_u, l_uu, l_ux - - def cost_final(self, x): - """ the final state cost function - - Parameters - ------------- - x : numpy.ndarray, shape(STATE_SIZE,) - predict state of the system - - Returns - --------- - l : float - terminal cost - l_x : numpy.ndarray, shape(STATE_SIZE, ) - differential of stage cost by x - l_xx : numpy.ndarray, shape(STATE_SIZE, STATE_SIZE) - second order differential of stage cost by x - """ - Q_11 = 10. # terminal x cost weight - Q_22 = 10. # terminal y cost weight - Q_33 = 0.1 # terminal theta cost weight - - error = self.simulator.xs - self.target - - l = np.dot(error.T, np.dot(np.diag([Q_11, Q_22, Q_33]), error)) - - # about L_x - l_x1 = 2. * (x[0] - self.target[0]) * Q_11 - l_x2 = 2. * (x[1] - self.target[1]) * Q_22 - l_x3 = 2. * (x[2] -self.target[2]) * Q_33 - l_x = np.array([l_x1, l_x2, l_x3]) - - # about l_xx - l_xx = 2. * np.diag([Q_11, Q_22, Q_33]) - - # Final cost only requires these three values - return l, l_x, l_xx - - def finite_differences(self, x, u): - """ calculate gradient of plant dynamics using finite differences - - Parameters - -------------- - x : numpy.ndarray, shape(STATE_SIZE,) - the state of the system - u : numpy.ndarray, shape(INPUT_SIZE,) - the control input - - Returns - ------------ - A : numpy.ndarray, shape(STATE_SIZE, STATE_SIZE) - differential of the model /alpha X - B : numpy.ndarray, shape(STATE_SIZE, INPUT_SIZE) - differential of the model /alpha U - - Notes - ------- - in this case, I pre-calculated the differential of the model because the tow-wheeled model is not difficult to calculate the gradient. - If you dont or cannot do that, you can use the numerical differentiation. - However, sometimes the the numerical differentiation affect the accuracy of calculations. - """ - - A = np.zeros((self.STATE_SIZE, self.STATE_SIZE)) - A_ideal = np.zeros((self.STATE_SIZE, self.STATE_SIZE)) - - B = np.zeros((self.STATE_SIZE, self.INPUT_SIZE)) - B_ideal = np.zeros((self.STATE_SIZE, self.INPUT_SIZE)) - - # if you want to use the numerical differentiation, please comment out this code - """ - eps = 1e-4 # finite differences epsilon - - for ii in range(self.STATE_SIZE): - # calculate partial differential w.r.t. x - inc_x = x.copy() - inc_x[ii] += eps - state_inc,_ = self.plant_dynamics(inc_x, u.copy()) - dec_x = x.copy() - dec_x[ii] -= eps - state_dec,_ = self.plant_dynamics(dec_x, u.copy()) - A[:, ii] = (state_inc - state_dec) / (2 * eps) - """ - - A_ideal[0, 2] = -np.sin(x[2]) * u[0] - A_ideal[1, 2] = np.cos(x[2]) * u[0] - - # if you want to use the numerical differentiation, please comment out this code - """ - for ii in range(self.INPUT_SIZE): - # calculate partial differential w.r.t. u - inc_u = u.copy() - inc_u[ii] += eps - state_inc,_ = self.plant_dynamics(x.copy(), inc_u) - dec_u = u.copy() - dec_u[ii] -= eps - state_dec,_ = self.plant_dynamics(x.copy(), dec_u) - B[:, ii] = (state_inc - state_dec) / (2 * eps) - """ - - B_ideal[0, 0] = np.cos(x[2]) - B_ideal[1, 0] = np.sin(x[2]) - B_ideal[2, 1] = 1. - - return A_ideal, B_ideal - - def ilqr(self, x0, U=None): - """ use iterative linear quadratic regulation to find a control - sequence that minimizes the cost function - - Parameters - -------------- - x0 : numpy.ndarray, shape(STATE_SIZE, ) - the initial state of the system - U : numpy.ndarray(TIME, INPUT_SIZE) - the initial control trajectory dimension - """ - U = self.U if U is None else U - - lamb = 1.0 # regularization parameter - sim_new_trajectory = True - tN = U.shape[0] # number of time steps - - for ii in range(self.max_iter): - - if sim_new_trajectory == True: - # simulate forward using the current control trajectory - X, cost = self.simulate(x0, U) - oldcost = np.copy(cost) # copy for exit condition check - - # - f_x = np.zeros((tN, self.STATE_SIZE, self.STATE_SIZE)) # df / dx - f_u = np.zeros((tN, self.STATE_SIZE, self.INPUT_SIZE)) # df / du - # for storing quadratized cost function - - l = np.zeros((tN,1)) # immediate state cost - l_x = np.zeros((tN, self.STATE_SIZE)) # dl / dx - l_xx = np.zeros((tN, self.STATE_SIZE, self.STATE_SIZE)) # d^2 l / dx^2 - l_u = np.zeros((tN, self.INPUT_SIZE)) # dl / du - l_uu = np.zeros((tN, self.INPUT_SIZE, self.INPUT_SIZE)) # d^2 l / du^2 - l_ux = np.zeros((tN, self.INPUT_SIZE, self.STATE_SIZE)) # d^2 l / du / dx - # for everything except final state - for t in range(tN-1): - # x(t+1) = f(x(t), u(t)) = x(t) + dx(t) * dt - # linearized dx(t) = np.dot(A(t), x(t)) + np.dot(B(t), u(t)) - # f_x = (np.eye + A(t)) * dt - # f_u = (B(t)) * dt - # continuous --> discrete - A, B = self.finite_differences(X[t], U[t]) - f_x[t] = np.eye(self.STATE_SIZE) + A * self.dt - f_u[t] = B * self.dt - - """ NOTE: why multiply dt in original program ?? - So the dt multiplication and + I is because we’re actually taking the derivative of the state with respect to the previous state. Which is not - dx = Ax + Bu, - but rather - x(t) = x(t-1) + (Ax(t-1) + Bu(t-1))*dt - And that’s where the identity matrix and dt come from! - So that part’s in the comments of the code, but the *dt on all the cost function stuff is not commented on at all! - So here the dt lets you normalize behaviour for different time steps (assuming you also scale the number of steps in the sequence). - So if you have a time step of .01 or .001 you’re not racking up 10 times as much cost function in the latter case. - And if you run the code with 50 steps in the sequence and dt=.01 and 500 steps in the sequence - and dt=.001 you’ll see that you get the same results, which is not the case at all when you don’t take dt into account in the cost function! - """ - - (l[t], l_x[t], l_xx[t], l_u[t], l_uu[t], l_ux[t]) = self.cost(X[t], U[t]) - - l[t] *= self.dt - l_x[t] *= self.dt - l_xx[t] *= self.dt - l_u[t] *= self.dt - l_uu[t] *= self.dt - l_ux[t] *= self.dt - - # and for final state - l[-1], l_x[-1], l_xx[-1] = self.cost_final(X[-1]) - - sim_new_trajectory = False - - V = l[-1].copy() # value function - V_x = l_x[-1].copy() # dV / dx - V_xx = l_xx[-1].copy() # d^2 V / dx^2 - k = np.zeros((tN, self.INPUT_SIZE)) # feedforward modification - K = np.zeros((tN, self.INPUT_SIZE, self.STATE_SIZE)) # feedback gain - - # work backwards to solve for V, Q, k, and K - for t in range(self.tN-2, -1, -1): - Q_x = l_x[t] + np.dot(f_x[t].T, V_x) - Q_u = l_u[t] + np.dot(f_u[t].T, V_x) - - Q_xx = l_xx[t] + np.dot(f_x[t].T, np.dot(V_xx, f_x[t])) - Q_ux = l_ux[t] + np.dot(f_u[t].T, np.dot(V_xx, f_x[t])) - Q_uu = l_uu[t] + np.dot(f_u[t].T, np.dot(V_xx, f_u[t])) - - Q_uu_evals, Q_uu_evecs = np.linalg.eig(Q_uu) - Q_uu_evals[Q_uu_evals < 0] = 0.0 - Q_uu_evals += lamb - Q_uu_inv = np.dot(Q_uu_evecs, np.dot(np.diag(1.0/Q_uu_evals), Q_uu_evecs.T)) - - k[t] = -1. * np.dot(Q_uu_inv, Q_u) - K[t] = -1. * np.dot(Q_uu_inv, Q_ux) - - V_x = Q_x - np.dot(K[t].T, np.dot(Q_uu, k[t])) - V_xx = Q_xx - np.dot(K[t].T, np.dot(Q_uu, K[t])) - - U_new = np.zeros((tN, self.INPUT_SIZE)) - x_new = x0.copy() - for t in range(tN - 1): - # use feedforward (k) and feedback (K) gain matrices - # calculated from our value function approximation - U_new[t] = U[t] + k[t] + np.dot(K[t], x_new - X[t]) - _,x_new = self.plant_dynamics(x_new, U_new[t]) - - # evaluate the new trajectory - X_new, cost_new = self.simulate(x0, U_new) - - if cost_new < cost: - # decrease lambda (get closer to Newton's method) - lamb /= self.lamb_factor - - X = np.copy(X_new) # update trajectory - U = np.copy(U_new) # update control signal - oldcost = np.copy(cost) - cost = np.copy(cost_new) - - sim_new_trajectory = True # do another rollout - - # check to see if update is small enough to exit - if ii > 0 and ((abs(oldcost-cost)/cost) < self.eps_converge): - print("Converged at iteration = %d; Cost = %.4f;"%(ii,cost_new) + - " logLambda = %.1f"%np.log(lamb)) - break - - else: - # increase lambda (get closer to gradient descent) - lamb *= self.lamb_factor - # print("cost: %.4f, increasing lambda to %.4f")%(cost, lamb) - if lamb > self.lamb_max: - print("lambda > max_lambda at iteration = %d;"%ii + - " Cost = %.4f; logLambda = %.1f"%(cost, - np.log(lamb))) - break - - return X, U, cost - - def plant_dynamics(self, x, u): - """ simulate a single time step of the plant, from - initial state x and applying control signal u - - Parameters - -------------- - x : numpy.ndarray, shape(STATE_SIZE, ) - the state of the system - u : numpy.ndarray, shape(INPUT_SIZE, ) - the control signal - - Returns - ----------- - xdot : numpy.ndarray, shape(STATE_SIZE, ) - the gradient of x - x_next : numpy.ndarray, shape(STATE_SIZE, ) - next state of x - """ - self.simulator.initialize_state(x) - - # apply the control signal - x_next = self.simulator.update_state(u, self.dt) - - # calculate the change in state - xdot = ((x_next - x) / self.dt).squeeze() - - return xdot, x_next - - def reset(self, target): - """ reset the state of the system """ - - # Index along current control sequence - self.t = 0 - self.U = np.zeros((self.tN, self.INPUT_SIZE)) - self.target = target.copy() - - def simulate(self, x0, U): - """ do a rollout of the system, starting at x0 and - applying the control sequence U - - Parameters - ---------- - x0 : numpy.ndarray, shape(STATE_SIZE, ) - the initial state of the system - U : numpy.ndarray, shape(tN, INPUT_SIZE) - the control sequence to apply - - Returns - --------- - X : numpy.ndarray, shape(tN, STATE_SIZE) - the state sequence - cost : float - cost - """ - - tN = U.shape[0] - X = np.zeros((tN, self.STATE_SIZE)) - X[0] = x0 - cost = 0 - - # Run simulation with substeps - for t in range(tN-1): - _,X[t+1] = self.plant_dynamics(X[t], U[t]) - l, _ , _, _ , _ , _ = self.cost(X[t], U[t]) - cost = cost + self.dt * l - - # Adjust for final cost, subsample trajectory - l_f, _, _ = self.cost_final(X[-1]) - cost = cost + l_f - - return X, cost diff --git a/ilqr/main_dynamic.py b/ilqr/main_dynamic.py deleted file mode 100644 index c6b22d6..0000000 --- a/ilqr/main_dynamic.py +++ /dev/null @@ -1,66 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math - -from model import TwoWheeledCar -from ilqr import iLQRController -from goal_maker import GoalMaker -from animation import AnimDrawer - -def main(): - """ - """ - # iteration parameters - NUM_ITERATIONS = 500 - dt = 0.01 - - # make plant - init_x = np.array([0., 0., 0.5*math.pi]) - car = TwoWheeledCar(init_x) - - # make goal - goal_maker = GoalMaker(goal_type="sin") - goal_maker.preprocess() - - # controller - controller = iLQRController() - - for iteration in range(NUM_ITERATIONS): - print("iteration num = {} / {}".format(iteration, NUM_ITERATIONS)) - - target = goal_maker.calc_goal(car.xs) - u = controller.calc_input(car, target) - car.update_state(u, dt) # update state - - # figures and animation - history_states = np.array(car.history_xs) - history_targets = np.array(goal_maker.history_target) - - time_fig = plt.figure() - - x_fig = time_fig.add_subplot(311) - y_fig = time_fig.add_subplot(312) - th_fig = time_fig.add_subplot(313) - - time = len(history_states) - - x_fig.plot(np.arange(time), history_states[:, 0], "r") - x_fig.plot(np.arange(1, time), history_targets[:, 0], "b", linestyle="dashdot") - x_fig.set_ylabel("x") - - y_fig.plot(np.arange(time), history_states[:, 1], "r") - y_fig.plot(np.arange(1, time), history_targets[:, 1], "b", linestyle="dashdot") - y_fig.set_ylabel("y") - - th_fig.plot(np.arange(time), history_states[:, 2], "r") - th_fig.plot(np.arange(1, time), history_targets[:, 2], "b", linestyle="dashdot") - th_fig.set_ylabel("th") - - plt.show() - - history_states = np.array(car.history_xs) - animdrawer = AnimDrawer([history_states, history_targets]) - animdrawer.draw_anim() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ilqr/main_static.py b/ilqr/main_static.py deleted file mode 100644 index 3cceb82..0000000 --- a/ilqr/main_static.py +++ /dev/null @@ -1,68 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math - -from model import TwoWheeledCar -from ilqr import iLQRController -from goal_maker import GoalMaker -from animation import AnimDrawer - - -def main(): - """ - """ - # iteration parameters - NUM_ITERATIONS = 500 - dt = 0.01 - - # make plant - init_x = np.array([0., 0., 0.5*math.pi]) - car = TwoWheeledCar(init_x) - - # make goal - goal_maker = GoalMaker(goal_type="const") - goal_maker.preprocess() - - # controller - controller = iLQRController() - - for iteration in range(NUM_ITERATIONS): - print("iteration num = {} / {}".format(iteration, NUM_ITERATIONS)) - - target = goal_maker.calc_goal(car.xs) - u = controller.calc_input(car, target) - car.update_state(u, dt) # update state - - # figures and animation - history_states = np.array(car.history_xs) - history_targets = np.array(goal_maker.history_target) - - time_fig = plt.figure() - - x_fig = time_fig.add_subplot(311) - y_fig = time_fig.add_subplot(312) - th_fig = time_fig.add_subplot(313) - - time = len(history_states) - - x_fig.plot(np.arange(time), history_states[:, 0], "r") - x_fig.plot(np.arange(1, time), history_targets[:, 0], "b", linestyle="dashdot") - x_fig.set_ylabel("x") - - y_fig.plot(np.arange(time), history_states[:, 1], "r") - y_fig.plot(np.arange(1, time), history_targets[:, 1], "b", linestyle="dashdot") - y_fig.set_ylabel("y") - - th_fig.plot(np.arange(time), history_states[:, 2], "r") - th_fig.plot(np.arange(1, time), history_targets[:, 2], "b", linestyle="dashdot") - th_fig.set_ylabel("th") - - plt.show() - - history_states = np.array(car.history_xs) - - animdrawer = AnimDrawer([history_states, history_targets]) - animdrawer.draw_anim() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/ilqr/model.py b/ilqr/model.py deleted file mode 100644 index 4030c8f..0000000 --- a/ilqr/model.py +++ /dev/null @@ -1,131 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - - -""" -このWheeled modelはコントローラー用 -ホントはbase作って、継承すべきですが省略 -""" -class TwoWheeledCar(): - """SampleSystem, this is the simulator - Attributes - ----------- - xs : numpy.ndarray - system states, [x, y, theta] - history_xs : list - time history of state - """ - def __init__(self, init_states=None): - """ - Palameters - ----------- - init_state : float, optional, shape(3, ) - initial state of system default is None - """ - self.STATE_SIZE = 3 - self.INPUT_SIZE = 2 - - self.xs = np.zeros(3) - - if init_states is not None: - self.xs = copy.deepcopy(init_states) - - self.history_xs = [init_states] - self.history_predict_xs = [] - - def update_state(self, us, dt): - """ - Palameters - ------------ - us : numpy.ndarray - inputs of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - # for theta 1, theta 1 dot, theta 2, theta 2 dot - k0 = [0.0 for _ in range(3)] - k1 = [0.0 for _ in range(3)] - k2 = [0.0 for _ in range(3)] - k3 = [0.0 for _ in range(3)] - - functions = [self._func_x_1, self._func_x_2, self._func_x_3] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(self.xs[0], self.xs[1], self.xs[2], us[0], us[1]) - - for i, func in enumerate(functions): - k1[i] = dt * func(self.xs[0] + k0[0]/2., self.xs[1] + k0[1]/2., self.xs[2] + k0[2]/2., us[0], us[1]) - - for i, func in enumerate(functions): - k2[i] = dt * func(self.xs[0] + k0[0]/2., self.xs[1] + k0[1]/2., self.xs[2] + k0[2]/2., us[0], us[1]) - - for i, func in enumerate(functions): - k3[i] = dt * func(self.xs[0] + k2[0], self.xs[1] + k2[1], self.xs[2] + k2[2], us[0], us[1]) - - self.xs[0] += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - self.xs[1] += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - self.xs[2] += (k0[2] + 2. * k1[2] + 2. * k2[2] + k3[2]) / 6. - - # save - save_states = copy.deepcopy(self.xs) - self.history_xs.append(save_states) - - return self.xs.copy() - - def initialize_state(self, init_xs): - """ - initialize the state - - Parameters - ------------ - init_xs : numpy.ndarray - """ - self.xs = init_xs.flatten() - - def _func_x_1(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.cos(y_3) * u_1 - return y_dot - - def _func_x_2(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.sin(y_3) * u_1 - return y_dot - - def _func_x_3(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = u_2 - return y_dot \ No newline at end of file diff --git a/mpc/basic/README.md b/mpc/basic/README.md deleted file mode 100644 index 0d64664..0000000 --- a/mpc/basic/README.md +++ /dev/null @@ -1,146 +0,0 @@ -# Model Predictive Control Basic Tool -This program is about template, generic function of linear model predictive control - -# Documentation of the MPC function -Linear model predicitive control should have state equation. -So if you want to use this function, you should model the plant as state equation. -Therefore, the parameters of this class are as following - -**class MpcController()** - -Attributes : - -- A : numpy.ndarray - - system matrix -- B : numpy.ndarray - - input matrix -- Q : numpy.ndarray - - evaluation function weight for states -- Qs : numpy.ndarray - - concatenated evaluation function weight for states -- R : numpy.ndarray - - evaluation function weight for inputs -- Rs : numpy.ndarray - - concatenated evaluation function weight for inputs -- pre_step : int - - prediction step -- state_size : int - - state size of the plant -- input_size : int - - input size of the plant -- dt_input_upper : numpy.ndarray, shape(input_size, ), optional - - constraints of input dt, default is None -- dt_input_lower : numpy.ndarray, shape(input_size, ), optional - - constraints of input dt, default is None -- input_upper : numpy.ndarray, shape(input_size, ), optional - - constraints of input, default is None -- input_lower : numpy.ndarray, shape(input_size, ), optional - - constraints of input, default is None - -Methods: - -- initialize_controller() initialize the controller -- calc_input(states, references) calculating optimal input - -More details, please look the **mpc_func_with_scipy.py** and **mpc_func_with_cvxopt.py** - -We have two function, mpc_func_with_cvxopt.py and mpc_func_with_scipy.py -Both functions have same variable and member function. However the solver is different. -Plese choose the right method for your environment. - -- example of import - -```py -from mpc_func_with_scipy import MpcController as MpcController_scipy -from mpc_func_with_cvxopt import MpcController as MpcController_cvxopt -``` - -# Examples -## Problem Formulation - -- **first order system** - - - -- **ACC (Adaptive cruise control)** - -The two wheeled model are expressed the following equation. - - - -However, if we assume the velocity are constant, we can approximate the equation as following, - - - -then we can apply this model to linear mpc, we should give the model reference V although. - -- **evaluation function** - -the both examples have same evaluation function form as following equation. - - - -- is predicit state by using predict input - -- is reference state - -- is predict amount of change of input - -- are evaluation function weights - -## Expected Results - -- first order system - -- time history - - - -- input - - - -- ACC (Adaptive cruise control) - -- time history of states - - - -- animation - - - - -# Usage - -- for example(first order system) - -``` -$ python main_example.py -``` - -- for example(ACC (Adaptive cruise control)) - -``` -$ python main_ACC.py -``` - -- for comparing two methods of optimization solvers - -``` -$ python test_compare_methods.py -``` - -# Requirement - -- python3.5 or more -- numpy -- matplotlib -- cvxopt -- scipy1.2.0 or more -- python-control - -# Reference -I`m sorry that main references are written in Japanese - -- モデル予測制御―制約のもとでの最適制御 著:Jan M. Maciejowski 訳:足立修一 東京電機大学出版局 \ No newline at end of file diff --git a/mpc/basic/animation.py b/mpc/basic/animation.py deleted file mode 100755 index 6ece541..0000000 --- a/mpc/basic/animation.py +++ /dev/null @@ -1,233 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.animation as ani -import matplotlib.font_manager as fon -import sys -import math - -# default setting of figures -plt.rcParams["mathtext.fontset"] = 'stix' # math fonts -plt.rcParams['xtick.direction'] = 'in' # x axis in -plt.rcParams['ytick.direction'] = 'in' # y axis in -plt.rcParams["font.size"] = 10 -plt.rcParams['axes.linewidth'] = 1.0 # axis line width -plt.rcParams['axes.grid'] = True # make grid - -def coordinate_transformation_in_angle(positions, base_angle): - ''' - Transformation the coordinate in the angle - - Parameters - ------- - positions : numpy.ndarray - this parameter is composed of xs, ys - should have (2, N) shape - base_angle : float [rad] - - Returns - ------- - traslated_positions : numpy.ndarray - the shape is (2, N) - - ''' - if positions.shape[0] != 2: - raise ValueError('the input data should have (2, N)') - - positions = np.array(positions) - positions = positions.reshape(2, -1) - - rot_matrix = [[np.cos(base_angle), np.sin(base_angle)], - [-1*np.sin(base_angle), np.cos(base_angle)]] - - rot_matrix = np.array(rot_matrix) - - translated_positions = np.dot(rot_matrix, positions) - - return translated_positions - -def square_make_with_angles(center_x, center_y, size, angle): - ''' - Create square matrix with angle line matrix(2D) - - Parameters - ------- - center_x : float in meters - the center x position of the square - center_y : float in meters - the center y position of the square - size : float in meters - the square's half-size - angle : float in radians - - Returns - ------- - square xs : numpy.ndarray - lenght is 5 (counterclockwise from right-up) - square ys : numpy.ndarray - length is 5 (counterclockwise from right-up) - angle line xs : numpy.ndarray - angle line ys : numpy.ndarray - ''' - - # start with the up right points - # create point in counterclockwise - square_xys = np.array([[size, 0.5 * size], [-size, 0.5 * size], [-size, -0.5 * size], [size, -0.5 * size], [size, 0.5 * size]]) - trans_points = coordinate_transformation_in_angle(square_xys.T, -angle) # this is inverse type - trans_points += np.array([[center_x], [center_y]]) - - square_xs = trans_points[0, :] - square_ys = trans_points[1, :] - - angle_line_xs = [center_x, center_x + math.cos(angle) * size] - angle_line_ys = [center_y, center_y + math.sin(angle) * size] - - return square_xs, square_ys, np.array(angle_line_xs), np.array(angle_line_ys) - - -class AnimDrawer(): - """create animation of path and robot - - Attributes - ------------ - cars : - anim_fig : figure of matplotlib - axis : axis of matplotlib - - """ - def __init__(self, objects): - """ - Parameters - ------------ - objects : list of objects - """ - self.lead_car_history_state = objects[0] - self.follow_car_history_state = objects[1] - - self.history_xs = [self.lead_car_history_state[:, 0], self.follow_car_history_state[:, 0]] - self.history_ys = [self.lead_car_history_state[:, 1], self.follow_car_history_state[:, 1]] - self.history_ths = [self.lead_car_history_state[:, 2], self.follow_car_history_state[:, 2]] - - # setting up figure - self.anim_fig = plt.figure(dpi=150) - self.axis = self.anim_fig.add_subplot(111) - - # imgs - self.object_imgs = [] - self.traj_imgs = [] - - def draw_anim(self, interval=50): - """draw the animation and save - - Parameteres - ------------- - interval : int, optional - animation's interval time, you should link the sampling time of systems - default is 50 [ms] - """ - self._set_axis() - self._set_img() - - self.skip_num = 1 - frame_num = int((len(self.history_xs[0])-1) / self.skip_num) - - animation = ani.FuncAnimation(self.anim_fig, self._update_anim, interval=interval, frames=frame_num) - - # self.axis.legend() - print('save_animation?') - shuold_save_animation = int(input()) - - if shuold_save_animation: - print('animation_number?') - num = int(input()) - animation.save('animation_{0}.mp4'.format(num), writer='ffmpeg') - # animation.save("Sample.gif", writer = 'imagemagick') # gif保存 - - plt.show() - - def _set_axis(self): - """ initialize the animation axies - """ - # (1) set the axis name - self.axis.set_xlabel(r'$\it{x}$ [m]') - self.axis.set_ylabel(r'$\it{y}$ [m]') - self.axis.set_aspect('equal', adjustable='box') - - # (2) set the xlim and ylim - self.axis.set_xlim(-5, 50) - self.axis.set_ylim(-2, 5) - - def _set_img(self): - """ initialize the imgs of animation - this private function execute the make initial imgs for animation - """ - # object imgs - obj_color_list = ["k", "k", "m", "m"] - obj_styles = ["solid", "solid", "solid", "solid"] - - for i in range(len(obj_color_list)): - temp_img, = self.axis.plot([], [], color=obj_color_list[i], linestyle=obj_styles[i]) - self.object_imgs.append(temp_img) - - traj_color_list = ["k", "m"] - - for i in range(len(traj_color_list)): - temp_img, = self.axis.plot([],[], color=traj_color_list[i], linestyle="dashed") - self.traj_imgs.append(temp_img) - - def _update_anim(self, i): - """the update animation - this function should be used in the animation functions - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - - Returns - ----------- - object_imgs : list of img - traj_imgs : list of img - """ - i = int(i * self.skip_num) - - self._draw_objects(i) - self._draw_traj(i) - - return self.object_imgs, self.traj_imgs, - - def _draw_objects(self, i): - """ - This private function is just divided thing of - the _update_anim to see the code more clear - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - """ - # cars - for j in range(2): - fix_j = j * 2 - object_x, object_y, angle_x, angle_y = square_make_with_angles(self.history_xs[j][i], - self.history_ys[j][i], - 1.0, - self.history_ths[j][i]) - - self.object_imgs[fix_j].set_data([object_x, object_y]) - self.object_imgs[fix_j + 1].set_data([angle_x, angle_y]) - - def _draw_traj(self, i): - """ - This private function is just divided thing of - the _update_anim to see the code more clear - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - """ - for j in range(2): - self.traj_imgs[j].set_data(self.history_xs[j][:i], self.history_ys[j][:i]) \ No newline at end of file diff --git a/mpc/basic/main_ACC.py b/mpc/basic/main_ACC.py deleted file mode 100644 index a868559..0000000 --- a/mpc/basic/main_ACC.py +++ /dev/null @@ -1,246 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from mpc_func_with_cvxopt import MpcController as MpcController_cvxopt -from animation import AnimDrawer -from control import matlab - -class TwoWheeledSystem(): - """SampleSystem, this is the simulator - Attributes - ----------- - xs : numpy.ndarray - system states, [x, y, theta] - history_xs : list - time history of state - """ - def __init__(self, init_states=None): - """ - Palameters - ----------- - init_state : float, optional, shape(3, ) - initial state of system default is None - """ - self.xs = np.zeros(3) - - if init_states is not None: - self.xs = copy.deepcopy(init_states) - - self.history_xs = [init_states] - - def update_state(self, us, dt=0.01): - """ - Palameters - ------------ - u : numpy.ndarray - inputs of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - # for theta 1, theta 1 dot, theta 2, theta 2 dot - k0 = [0.0 for _ in range(3)] - k1 = [0.0 for _ in range(3)] - k2 = [0.0 for _ in range(3)] - k3 = [0.0 for _ in range(3)] - - functions = [self._func_x_1, self._func_x_2, self._func_x_3] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(self.xs[0], self.xs[1], self.xs[2], us[0], us[1]) - - for i, func in enumerate(functions): - k1[i] = dt * func(self.xs[0] + k0[0]/2., self.xs[1] + k0[1]/2., self.xs[2] + k0[2]/2., us[0], us[1]) - - for i, func in enumerate(functions): - k2[i] = dt * func(self.xs[0] + k0[0]/2., self.xs[1] + k0[1]/2., self.xs[2] + k0[2]/2., us[0], us[1]) - - for i, func in enumerate(functions): - k3[i] = dt * func(self.xs[0] + k2[0], self.xs[1] + k2[1], self.xs[2] + k2[2], us[0], us[1]) - - self.xs[0] += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - self.xs[1] += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - self.xs[2] += (k0[2] + 2. * k1[2] + 2. * k2[2] + k3[2]) / 6. - - # save - save_states = copy.deepcopy(self.xs) - self.history_xs.append(save_states) - print(self.xs) - - def _func_x_1(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.cos(y_3) * u_1 - return y_dot - - def _func_x_2(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.sin(y_3) * u_1 - return y_dot - - def _func_x_3(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = u_2 - return y_dot - -def main(): - dt = 0.05 - simulation_time = 10 # in seconds - iteration_num = int(simulation_time / dt) - - # you must be care about this matrix - # these A and B are for continuos system if you want to use discret system matrix please skip this step - # lineared car system - V = 5.0 - A = np.array([[0., V], [0., 0.]]) - B = np.array([[0.], [1.]]) - - C = np.eye(2) - D = np.zeros((2, 1)) - - # make simulator with coninuous matrix - init_xs_lead = np.array([5., 0., 0.]) - init_xs_follow = np.array([0., 0., 0.]) - lead_car = TwoWheeledSystem(init_states=init_xs_lead) - follow_car = TwoWheeledSystem(init_states=init_xs_follow) - - # create system - sysc = matlab.ss(A, B, C, D) - # discrete system - sysd = matlab.c2d(sysc, dt) - - Ad = sysd.A - Bd = sysd.B - - # evaluation function weight - Q = np.diag([1., 1.]) - R = np.diag([5.]) - pre_step = 15 - - # make controller with discreted matrix - # please check the solver, if you want to use the scipy, set the MpcController_scipy - lead_controller = MpcController_cvxopt(Ad, Bd, Q, R, pre_step, - dt_input_upper=np.array([30 * dt]), dt_input_lower=np.array([-30 * dt]), - input_upper=np.array([30.]), input_lower=np.array([-30.])) - - follow_controller = MpcController_cvxopt(Ad, Bd, Q, R, pre_step, - dt_input_upper=np.array([30 * dt]), dt_input_lower=np.array([-30 * dt]), - input_upper=np.array([30.]), input_lower=np.array([-30.])) - - lead_controller.initialize_controller() - follow_controller.initialize_controller() - - # reference - lead_reference = np.array([[0., 0.] for _ in range(pre_step)]).flatten() - - for i in range(iteration_num): - print("simulation time = {0}".format(i)) - # make lead car's move - if i > int(iteration_num / 3): - lead_reference = np.array([[4., 0.] for _ in range(pre_step)]).flatten() - - lead_states = lead_car.xs - lead_opt_u = lead_controller.calc_input(lead_states[1:], lead_reference) - lead_opt_u = np.hstack((np.array([V]), lead_opt_u)) - - # make follow car - follow_reference = np.array([lead_states[1:] for _ in range(pre_step)]).flatten() - follow_states = follow_car.xs - - follow_opt_u = follow_controller.calc_input(follow_states[1:], follow_reference) - follow_opt_u = np.hstack((np.array([V]), follow_opt_u)) - - lead_car.update_state(lead_opt_u, dt=dt) - follow_car.update_state(follow_opt_u, dt=dt) - - # figures and animation - lead_history_states = np.array(lead_car.history_xs) - follow_history_states = np.array(follow_car.history_xs) - - time_history_fig = plt.figure() - x_fig = time_history_fig.add_subplot(311) - y_fig = time_history_fig.add_subplot(312) - theta_fig = time_history_fig.add_subplot(313) - - car_traj_fig = plt.figure() - traj_fig = car_traj_fig.add_subplot(111) - traj_fig.set_aspect('equal') - - x_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 0], label="lead") - x_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 0], label="follow") - x_fig.set_xlabel("time [s]") - x_fig.set_ylabel("x") - x_fig.legend() - - y_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 1], label="lead") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 1], label="follow") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), [4. for _ in range(iteration_num+1)], linestyle="dashed") - y_fig.set_xlabel("time [s]") - y_fig.set_ylabel("y") - y_fig.legend() - - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 2], label="lead") - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 2], label="follow") - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), [0. for _ in range(iteration_num+1)], linestyle="dashed") - theta_fig.set_xlabel("time [s]") - theta_fig.set_ylabel("theta") - theta_fig.legend() - - time_history_fig.tight_layout() - - traj_fig.plot(lead_history_states[:, 0], lead_history_states[:, 1], label="lead") - traj_fig.plot(follow_history_states[:, 0], follow_history_states[:, 1], label="follow") - traj_fig.set_xlabel("x") - traj_fig.set_ylabel("y") - traj_fig.legend() - plt.show() - - lead_history_us = np.array(lead_controller.history_us) - follow_history_us = np.array(follow_controller.history_us) - input_history_fig = plt.figure() - u_1_fig = input_history_fig.add_subplot(111) - - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_us[:, 0], label="lead") - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_us[:, 0], label="follow") - u_1_fig.set_xlabel("time [s]") - u_1_fig.set_ylabel("u_omega") - - input_history_fig.tight_layout() - plt.show() - - animdrawer = AnimDrawer([lead_history_states, follow_history_states]) - animdrawer.draw_anim() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/mpc/basic/main_ACC_TEMP.py b/mpc/basic/main_ACC_TEMP.py deleted file mode 100644 index 9ad7c97..0000000 --- a/mpc/basic/main_ACC_TEMP.py +++ /dev/null @@ -1,243 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from mpc_func_with_cvxopt import MpcController as MpcController_cvxopt -from animation import AnimDrawer -# from control import matlab - -class TwoWheeledSystem(): - """SampleSystem, this is the simulator - Attributes - ----------- - xs : numpy.ndarray - system states, [x, y, theta] - history_xs : list - time history of state - """ - def __init__(self, init_states=None): - """ - Palameters - ----------- - init_state : float, optional, shape(3, ) - initial state of system default is None - """ - self.xs = np.zeros(3) - - if init_states is not None: - self.xs = copy.deepcopy(init_states) - - self.history_xs = [init_states] - - def update_state(self, us, dt=0.01): - """ - Palameters - ------------ - u : numpy.ndarray - inputs of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - # for theta 1, theta 1 dot, theta 2, theta 2 dot - k0 = [0.0 for _ in range(3)] - k1 = [0.0 for _ in range(3)] - k2 = [0.0 for _ in range(3)] - k3 = [0.0 for _ in range(3)] - - functions = [self._func_x_1, self._func_x_2, self._func_x_3] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(self.xs[0], self.xs[1], self.xs[2], us[0], us[1]) - - for i, func in enumerate(functions): - k1[i] = dt * func(self.xs[0] + k0[0]/2., self.xs[1] + k0[1]/2., self.xs[2] + k0[2]/2., us[0], us[1]) - - for i, func in enumerate(functions): - k2[i] = dt * func(self.xs[0] + k0[0]/2., self.xs[1] + k0[1]/2., self.xs[2] + k0[2]/2., us[0], us[1]) - - for i, func in enumerate(functions): - k3[i] = dt * func(self.xs[0] + k2[0], self.xs[1] + k2[1], self.xs[2] + k2[2], us[0], us[1]) - - self.xs[0] += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - self.xs[1] += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - self.xs[2] += (k0[2] + 2. * k1[2] + 2. * k2[2] + k3[2]) / 6. - - # save - save_states = copy.deepcopy(self.xs) - self.history_xs.append(save_states) - print(self.xs) - - def _func_x_1(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.cos(y_3) * u_1 - return y_dot - - def _func_x_2(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.sin(y_3) * u_1 - return y_dot - - def _func_x_3(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = u_2 - return y_dot - -def main(): - dt = 0.05 - simulation_time = 10 # in seconds - iteration_num = int(simulation_time / dt) - - # you must be care about this matrix - # these A and B are for continuos system if you want to use discret system matrix please skip this step - # lineared car system - V = 5.0 - Ad = np.array([[1., V * dt], [0., 1.]]) - Bd = np.array([[0.], [1. * dt]]) - - C = np.eye(2) - D = np.zeros((2, 1)) - - # make simulator with coninuous matrix - init_xs_lead = np.array([5., 0., 0.]) - init_xs_follow = np.array([0., 0., 0.]) - lead_car = TwoWheeledSystem(init_states=init_xs_lead) - follow_car = TwoWheeledSystem(init_states=init_xs_follow) - - # create system - # sysc = matlab.ss(A, B, C, D) - # discrete system - # sysd = matlab.c2d(sysc, dt) - - # evaluation function weight - Q = np.diag([1., 1.]) - R = np.diag([5.]) - pre_step = 15 - - # make controller with discreted matrix - # please check the solver, if you want to use the scipy, set the MpcController_scipy - lead_controller = MpcController_cvxopt(Ad, Bd, Q, R, pre_step, - dt_input_upper=np.array([30 * dt]), dt_input_lower=np.array([-30 * dt]), - input_upper=np.array([30.]), input_lower=np.array([-30.])) - - follow_controller = MpcController_cvxopt(Ad, Bd, Q, R, pre_step, - dt_input_upper=np.array([30 * dt]), dt_input_lower=np.array([-30 * dt]), - input_upper=np.array([30.]), input_lower=np.array([-30.])) - - lead_controller.initialize_controller() - follow_controller.initialize_controller() - - # reference - lead_reference = np.array([[0., 0.] for _ in range(pre_step)]).flatten() - - for i in range(iteration_num): - print("simulation time = {0}".format(i)) - # make lead car's move - if i > int(iteration_num / 3): - lead_reference = np.array([[4., 0.] for _ in range(pre_step)]).flatten() - - lead_states = lead_car.xs - lead_opt_u = lead_controller.calc_input(lead_states[1:], lead_reference) - lead_opt_u = np.hstack((np.array([V]), lead_opt_u)) - - # make follow car - follow_reference = np.array([lead_states[1:] for _ in range(pre_step)]).flatten() - follow_states = follow_car.xs - - follow_opt_u = follow_controller.calc_input(follow_states[1:], follow_reference) - follow_opt_u = np.hstack((np.array([V]), follow_opt_u)) - - lead_car.update_state(lead_opt_u, dt=dt) - follow_car.update_state(follow_opt_u, dt=dt) - - # figures and animation - lead_history_states = np.array(lead_car.history_xs) - follow_history_states = np.array(follow_car.history_xs) - - time_history_fig = plt.figure() - x_fig = time_history_fig.add_subplot(311) - y_fig = time_history_fig.add_subplot(312) - theta_fig = time_history_fig.add_subplot(313) - - car_traj_fig = plt.figure() - traj_fig = car_traj_fig.add_subplot(111) - traj_fig.set_aspect('equal') - - x_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 0], label="lead") - x_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 0], label="follow") - x_fig.set_xlabel("time [s]") - x_fig.set_ylabel("x") - x_fig.legend() - - y_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 1], label="lead") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 1], label="follow") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), [4. for _ in range(iteration_num+1)], linestyle="dashed") - y_fig.set_xlabel("time [s]") - y_fig.set_ylabel("y") - y_fig.legend() - - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 2], label="lead") - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 2], label="follow") - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), [0. for _ in range(iteration_num+1)], linestyle="dashed") - theta_fig.set_xlabel("time [s]") - theta_fig.set_ylabel("theta") - theta_fig.legend() - - time_history_fig.tight_layout() - - traj_fig.plot(lead_history_states[:, 0], lead_history_states[:, 1], label="lead") - traj_fig.plot(follow_history_states[:, 0], follow_history_states[:, 1], label="follow") - traj_fig.set_xlabel("x") - traj_fig.set_ylabel("y") - traj_fig.legend() - plt.show() - - lead_history_us = np.array(lead_controller.history_us) - follow_history_us = np.array(follow_controller.history_us) - input_history_fig = plt.figure() - u_1_fig = input_history_fig.add_subplot(111) - - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_us[:, 0], label="lead") - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_us[:, 0], label="follow") - u_1_fig.set_xlabel("time [s]") - u_1_fig.set_ylabel("u_omega") - - input_history_fig.tight_layout() - plt.show() - - animdrawer = AnimDrawer([lead_history_states, follow_history_states]) - animdrawer.draw_anim() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/mpc/basic/main_example.py b/mpc/basic/main_example.py deleted file mode 100644 index b768a21..0000000 --- a/mpc/basic/main_example.py +++ /dev/null @@ -1,188 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from mpc_func_with_scipy import MpcController as MpcController_scipy -from mpc_func_with_cvxopt import MpcController as MpcController_cvxopt -from control import matlab - -class FirstOrderSystem(): - """FirstOrderSystemWithStates - - Attributes - ----------- - xs : numpy.ndarray - system states - A : numpy.ndarray - system matrix - B : numpy.ndarray - control matrix - C : numpy.ndarray - observation matrix - history_xs : list - time history of state - """ - def __init__(self, A, B, C, D=None, init_states=None): - """ - Parameters - ----------- - A : numpy.ndarray - system matrix - B : numpy.ndarray - control matrix - C : numpy.ndarray - observation matrix - D : numpy.ndarray - directly matrix - init_state : float, optional - initial state of system default is None - history_xs : list - time history of system states - """ - - self.A = A - self.B = B - self.C = C - - if D is not None: - self.D = D - - self.xs = np.zeros(self.A.shape[0]) - - if init_states is not None: - self.xs = copy.deepcopy(init_states) - - self.history_xs = [init_states] - - def update_state(self, u, dt=0.01): - """calculating input - Parameters - ------------ - u : numpy.ndarray - inputs of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - temp_x = self.xs.reshape(-1, 1) - temp_u = u.reshape(-1, 1) - - # solve Runge-Kutta - k0 = dt * (np.dot(self.A, temp_x) + np.dot(self.B, temp_u)) - k1 = dt * (np.dot(self.A, temp_x + k0/2.) + np.dot(self.B, temp_u)) - k2 = dt * (np.dot(self.A, temp_x + k1/2.) + np.dot(self.B, temp_u)) - k3 = dt * (np.dot(self.A, temp_x + k2) + np.dot(self.B, temp_u)) - - self.xs += ((k0 + 2 * k1 + 2 * k2 + k3) / 6.).flatten() - - # for oylar - # self.xs += k0.flatten() - # print("xs = {0}".format(self.xs)) - - # save - save_states = copy.deepcopy(self.xs) - self.history_xs.append(save_states) - -def main(): - dt = 0.05 - simulation_time = 30 # in seconds - iteration_num = int(simulation_time / dt) - - # you must be care about this matrix - # these A and B are for continuos system if you want to use discret system matrix please skip this step - tau = 0.63 - A = np.array([[-1./tau, 0., 0., 0.], - [0., -1./tau, 0., 0.], - [1., 0., 0., 0.], - [0., 1., 0., 0.]]) - B = np.array([[1./tau, 0.], - [0., 1./tau], - [0., 0.], - [0., 0.]]) - - C = np.eye(4) - D = np.zeros((4, 2)) - - # make simulator with coninuous matrix - init_xs = np.array([0., 0., 0., 0.]) - plant = FirstOrderSystem(A, B, C, init_states=init_xs) - - # create system - sysc = matlab.ss(A, B, C, D) - # discrete system - sysd = matlab.c2d(sysc, dt) - - Ad = sysd.A - Bd = sysd.B - - print(Ad) - print(Bd) - input() - - # evaluation function weight - Q = np.diag([1., 1., 1., 1.]) - R = np.diag([1., 1.]) - pre_step = 10 - - # make controller with discreted matrix - # please check the solver, if you want to use the scipy, set the MpcController_scipy - controller = MpcController_cvxopt(Ad, Bd, Q, R, pre_step, - dt_input_upper=np.array([0.25 * dt, 0.25 * dt]), dt_input_lower=np.array([-0.5 * dt, -0.5 * dt]), - input_upper=np.array([1. ,3.]), input_lower=np.array([-1., -3.])) - - controller.initialize_controller() - - for i in range(iteration_num): - print("simulation time = {0}".format(i)) - reference = np.array([[0., 0., -5., 7.5] for _ in range(pre_step)]).flatten() - states = plant.xs - opt_u = controller.calc_input(states, reference) - plant.update_state(opt_u, dt=dt) - - history_states = np.array(plant.history_xs) - - time_history_fig = plt.figure() - x_fig = time_history_fig.add_subplot(411) - y_fig = time_history_fig.add_subplot(412) - v_x_fig = time_history_fig.add_subplot(413) - v_y_fig = time_history_fig.add_subplot(414) - - v_x_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states[:, 0]) - v_x_fig.plot(np.arange(0, simulation_time+0.01, dt), [0. for _ in range(iteration_num+1)], linestyle="dashed") - v_x_fig.set_xlabel("time [s]") - v_x_fig.set_ylabel("v_x") - - v_y_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states[:, 1]) - v_y_fig.plot(np.arange(0, simulation_time+0.01, dt), [0. for _ in range(iteration_num+1)], linestyle="dashed") - v_y_fig.set_xlabel("time [s]") - v_y_fig.set_ylabel("v_y") - - x_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states[:, 2]) - x_fig.plot(np.arange(0, simulation_time+0.01, dt), [-5. for _ in range(iteration_num+1)], linestyle="dashed") - x_fig.set_xlabel("time [s]") - x_fig.set_ylabel("x") - - y_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states[:, 3]) - y_fig.plot(np.arange(0, simulation_time+0.01, dt), [7.5 for _ in range(iteration_num+1)], linestyle="dashed") - y_fig.set_xlabel("time [s]") - y_fig.set_ylabel("y") - time_history_fig.tight_layout() - plt.show() - - history_us = np.array(controller.history_us) - input_history_fig = plt.figure() - u_1_fig = input_history_fig.add_subplot(211) - u_2_fig = input_history_fig.add_subplot(212) - - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), history_us[:, 0]) - u_1_fig.set_xlabel("time [s]") - u_1_fig.set_ylabel("u_x") - - u_2_fig.plot(np.arange(0, simulation_time+0.01, dt), history_us[:, 1]) - u_2_fig.set_xlabel("time [s]") - u_2_fig.set_ylabel("u_y") - input_history_fig.tight_layout() - plt.show() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/mpc/basic/mpc_func_with_cvxopt.py b/mpc/basic/mpc_func_with_cvxopt.py deleted file mode 100644 index 4e04736..0000000 --- a/mpc/basic/mpc_func_with_cvxopt.py +++ /dev/null @@ -1,256 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from cvxopt import matrix, solvers - -class MpcController(): - """ - Attributes - ------------ - A : numpy.ndarray - system matrix - B : numpy.ndarray - input matrix - Q : numpy.ndarray - evaluation function weight for states - Qs : numpy.ndarray - concatenated evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - Rs : numpy.ndarray - concatenated evaluation function weight for inputs - pre_step : int - prediction step - state_size : int - state size of the plant - input_size : int - input size of the plant - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - """ - def __init__(self, A, B, Q, R, pre_step, initial_input=None, dt_input_upper=None, dt_input_lower=None, input_upper=None, input_lower=None): - """ - Parameters - ------------ - A : numpy.ndarray - system matrix - B : numpy.ndarray - input matrix - Q : numpy.ndarray - evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - pre_step : int - prediction step - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - history_us : list - time history of optimal input us(numpy.ndarray) - """ - self.A = np.array(A) - self.B = np.array(B) - self.Q = np.array(Q) - self.R = np.array(R) - self.pre_step = pre_step - - self.Qs = None - self.Rs = None - - self.state_size = self.A.shape[0] - self.input_size = self.B.shape[1] - - self.history_us = [np.zeros(self.input_size)] - - # initial state - if initial_input is not None: - self.history_us = [initial_input] - - # constraints - self.dt_input_lower = dt_input_lower - self.dt_input_upper = dt_input_upper - self.input_upper = input_upper - self.input_lower = input_lower - - # about mpc matrix - self.W = None - self.omega = None - self.F = None - self.f = None - - def initialize_controller(self): - """ - make matrix to calculate optimal control input - - """ - A_factorials = [self.A] - - self.phi_mat = copy.deepcopy(self.A) - - for _ in range(self.pre_step - 1): - temp_mat = np.dot(A_factorials[-1], self.A) - self.phi_mat = np.vstack((self.phi_mat, temp_mat)) - - A_factorials.append(temp_mat) # after we use this factorials - - print("phi_mat = \n{0}".format(self.phi_mat)) - - self.gamma_mat = copy.deepcopy(self.B) - gammma_mat_temp = copy.deepcopy(self.B) - - for i in range(self.pre_step - 1): - temp_1_mat = np.dot(A_factorials[i], self.B) - gammma_mat_temp = temp_1_mat + gammma_mat_temp - self.gamma_mat = np.vstack((self.gamma_mat, gammma_mat_temp)) - - print("gamma_mat = \n{0}".format(self.gamma_mat)) - - self.theta_mat = copy.deepcopy(self.gamma_mat) - - for i in range(self.pre_step - 1): - temp_mat = np.zeros_like(self.gamma_mat) - temp_mat[int((i + 1)*self.state_size): , :] = self.gamma_mat[:-int((i + 1)*self.state_size) , :] - - self.theta_mat = np.hstack((self.theta_mat, temp_mat)) - - print("theta_mat = \n{0}".format(self.theta_mat)) - - # evaluation function weight - diag_Qs = np.array([np.diag(self.Q) for _ in range(self.pre_step)]) - diag_Rs = np.array([np.diag(self.R) for _ in range(self.pre_step)]) - - self.Qs = np.diag(diag_Qs.flatten()) - self.Rs = np.diag(diag_Rs.flatten()) - - print("Qs = \n{0}".format(self.Qs)) - print("Rs = \n{0}".format(self.Rs)) - - # constraints - # about dt U - if self.input_lower is not None: - # initialize - self.F = np.zeros((self.input_size * 2, self.pre_step * self.input_size)) - for i in range(self.input_size): - self.F[i * 2: (i + 1) * 2, i] = np.array([1., -1.]) - temp_F = copy.deepcopy(self.F) - - print("F = \n{0}".format(self.F)) - - for i in range(self.pre_step - 1): - temp_F = copy.deepcopy(temp_F) - - for j in range(self.input_size): - temp_F[j * 2: (j + 1) * 2, ((i+1) * self.input_size) + j] = np.array([1., -1.]) - - self.F = np.vstack((self.F, temp_F)) - - self.F1 = self.F[:, :self.input_size] - - temp_f = [] - - for i in range(self.input_size): - temp_f.append(-1 * self.input_upper[i]) - temp_f.append(self.input_lower[i]) - - self.f = np.array([temp_f for _ in range(self.pre_step)]).flatten() - - print("F = \n{0}".format(self.F)) - print("F1 = \n{0}".format(self.F1)) - print("f = \n{0}".format(self.f)) - - # about dt_u - if self.dt_input_lower is not None: - self.W = np.zeros((2, self.pre_step * self.input_size)) - self.W[:, 0] = np.array([1., -1.]) - - for i in range(self.pre_step * self.input_size - 1): - temp_W = np.zeros((2, self.pre_step * self.input_size)) - temp_W[:, i+1] = np.array([1., -1.]) - self.W = np.vstack((self.W, temp_W)) - - temp_omega = [] - - for i in range(self.input_size): - temp_omega.append(self.dt_input_upper[i]) - temp_omega.append(-1. * self.dt_input_lower[i]) - - self.omega = np.array([temp_omega for _ in range(self.pre_step)]).flatten() - - print("W = \n{0}".format(self.W)) - print("omega = \n{0}".format(self.omega)) - - # about state - print("check the matrix!! if you think rite, plese push enter") - input() - - def calc_input(self, states, references): - """calculate optimal input - Parameters - ----------- - states : numpy.ndarray, shape(state length, ) - current state of system - references : numpy.ndarray, shape(state length * pre_step, ) - reference of the system, you should set this value as reachable goal - - References - ------------ - opt_input : numpy.ndarray, shape(input_length, ) - optimal input - """ - temp_1 = np.dot(self.phi_mat, states.reshape(-1, 1)) - temp_2 = np.dot(self.gamma_mat, self.history_us[-1].reshape(-1, 1)) - - error = references.reshape(-1, 1) - temp_1 - temp_2 - - G = 2. * np.dot(self.theta_mat.T, np.dot(self.Qs, error)) - - H = np.dot(self.theta_mat.T, np.dot(self.Qs, self.theta_mat)) + self.Rs - - # constraints - A = [] - b = [] - - if self.W is not None: - A.append(self.W) - b.append(self.omega.reshape(-1, 1)) - - if self.F is not None: - b_F = - np.dot(self.F1, self.history_us[-1].reshape(-1, 1)) - self.f.reshape(-1, 1) - A.append(self.F) - b.append(b_F) - - A = np.array(A).reshape(-1, self.input_size * self.pre_step) - - ub = np.array(b).flatten() - - # make cvxpy problem formulation - P = 2*matrix(H) - q = matrix(-1 * G) - A = matrix(A) - b = matrix(ub) - - # constraint - if self.W is not None or self.F is not None : - opt_result = solvers.qp(P, q, G=A, h=b) - - opt_dt_us = list(opt_result['x']) - - opt_u = opt_dt_us[:self.input_size] + self.history_us[-1] - - # save - self.history_us.append(opt_u) - - return opt_u \ No newline at end of file diff --git a/mpc/basic/mpc_func_with_scipy.py b/mpc/basic/mpc_func_with_scipy.py deleted file mode 100644 index 54ef3c9..0000000 --- a/mpc/basic/mpc_func_with_scipy.py +++ /dev/null @@ -1,262 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from scipy.optimize import minimize -from scipy.optimize import LinearConstraint - -class MpcController(): - """ - Attributes - ------------ - A : numpy.ndarray - system matrix - B : numpy.ndarray - input matrix - Q : numpy.ndarray - evaluation function weight for states - Qs : numpy.ndarray - concatenated evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - Rs : numpy.ndarray - concatenated evaluation function weight for inputs - pre_step : int - prediction step - state_size : int - state size of the plant - input_size : int - input size of the plant - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - """ - def __init__(self, A, B, Q, R, pre_step, initial_input=None, dt_input_upper=None, dt_input_lower=None, input_upper=None, input_lower=None): - """ - Parameters - ------------ - A : numpy.ndarray - system matrix - B : numpy.ndarray - input matrix - Q : numpy.ndarray - evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - pre_step : int - prediction step - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - history_us : list - time history of optimal input us(numpy.ndarray) - """ - self.A = np.array(A) - self.B = np.array(B) - self.Q = np.array(Q) - self.R = np.array(R) - self.pre_step = pre_step - - self.Qs = None - self.Rs = None - - self.state_size = self.A.shape[0] - self.input_size = self.B.shape[1] - - self.history_us = [np.zeros(self.input_size)] - - # initial state - if initial_input is not None: - self.history_us = [initial_input] - - # constraints - self.dt_input_lower = dt_input_lower - self.dt_input_upper = dt_input_upper - self.input_upper = input_upper - self.input_lower = input_lower - - self.W = None - self.omega = None - self.F = None - self.f = None - - def initialize_controller(self): - """ - make matrix to calculate optimal control input - """ - A_factorials = [self.A] - - self.phi_mat = copy.deepcopy(self.A) - - for _ in range(self.pre_step - 1): - temp_mat = np.dot(A_factorials[-1], self.A) - self.phi_mat = np.vstack((self.phi_mat, temp_mat)) - - A_factorials.append(temp_mat) # after we use this factorials - - print("phi_mat = \n{0}".format(self.phi_mat)) - - self.gamma_mat = copy.deepcopy(self.B) - gammma_mat_temp = copy.deepcopy(self.B) - - for i in range(self.pre_step - 1): - temp_1_mat = np.dot(A_factorials[i], self.B) - gammma_mat_temp = temp_1_mat + gammma_mat_temp - self.gamma_mat = np.vstack((self.gamma_mat, gammma_mat_temp)) - - print("gamma_mat = \n{0}".format(self.gamma_mat)) - - self.theta_mat = copy.deepcopy(self.gamma_mat) - - for i in range(self.pre_step - 1): - temp_mat = np.zeros_like(self.gamma_mat) - temp_mat[int((i + 1)*self.state_size): , :] = self.gamma_mat[:-int((i + 1)*self.state_size) , :] - - self.theta_mat = np.hstack((self.theta_mat, temp_mat)) - - print("theta_mat = \n{0}".format(self.theta_mat)) - - # evaluation function weight - diag_Qs = np.array([np.diag(self.Q) for _ in range(self.pre_step)]) - diag_Rs = np.array([np.diag(self.R) for _ in range(self.pre_step)]) - - self.Qs = np.diag(diag_Qs.flatten()) - self.Rs = np.diag(diag_Rs.flatten()) - - print("Qs = \n{0}".format(self.Qs)) - print("Rs = \n{0}".format(self.Rs)) - - # constraints - # about dt U - if self.input_lower is not None: - # initialize - self.F = np.zeros((self.input_size * 2, self.pre_step * self.input_size)) - for i in range(self.input_size): - self.F[i * 2: (i + 1) * 2, i] = np.array([1., -1.]) - temp_F = copy.deepcopy(self.F) - - print("F = \n{0}".format(self.F)) - - for i in range(self.pre_step - 1): - temp_F = copy.deepcopy(temp_F) - - for j in range(self.input_size): - temp_F[j * 2: (j + 1) * 2, ((i+1) * self.input_size) + j] = np.array([1., -1.]) - - self.F = np.vstack((self.F, temp_F)) - - self.F1 = self.F[:, :self.input_size] - - temp_f = [] - - for i in range(self.input_size): - temp_f.append(-1 * self.input_upper[i]) - temp_f.append(self.input_lower[i]) - - self.f = np.array([temp_f for _ in range(self.pre_step)]).flatten() - - print("F = \n{0}".format(self.F)) - print("F1 = \n{0}".format(self.F1)) - print("f = \n{0}".format(self.f)) - - # about dt_u - if self.dt_input_lower is not None: - self.W = np.zeros((2, self.pre_step * self.input_size)) - self.W[:, 0] = np.array([1., -1.]) - - for i in range(self.pre_step * self.input_size - 1): - temp_W = np.zeros((2, self.pre_step * self.input_size)) - temp_W[:, i+1] = np.array([1., -1.]) - self.W = np.vstack((self.W, temp_W)) - - temp_omega = [] - - for i in range(self.input_size): - temp_omega.append(self.dt_input_upper[i]) - temp_omega.append(-1. * self.dt_input_lower[i]) - - self.omega = np.array([temp_omega for _ in range(self.pre_step)]).flatten() - - print("W = \n{0}".format(self.W)) - print("omega = \n{0}".format(self.omega)) - - # about state - print("check the matrix!! if you think rite, plese push enter") - input() - - def calc_input(self, states, references): - """calculate optimal input - Parameters - ----------- - states : numpy.ndarray, shape(state length, ) - current state of system - references : numpy.ndarray, shape(state length * pre_step, ) - reference of the system, you should set this value as reachable goal - - References - ------------ - opt_input : numpy.ndarray, shape(input_length, ) - optimal input - """ - temp_1 = np.dot(self.phi_mat, states.reshape(-1, 1)) - temp_2 = np.dot(self.gamma_mat, self.history_us[-1].reshape(-1, 1)) - - error = references.reshape(-1, 1) - temp_1 - temp_2 - - G = 2. * np.dot(self.theta_mat.T, np.dot(self.Qs, error) ) - - H = np.dot(self.theta_mat.T, np.dot(self.Qs, self.theta_mat)) + self.Rs - - # constraints - A = [] - b = [] - - if self.W is not None: - A.append(self.W) - b.append(self.omega.reshape(-1, 1)) - - if self.F is not None: - b_F = - np.dot(self.F1, self.history_us[-1].reshape(-1, 1)) - self.f.reshape(-1, 1) - A.append(self.F) - b.append(b_F) - - A = np.array(A).reshape(-1, self.input_size * self.pre_step) - - ub = np.array(b).flatten() - - def optimized_func(dt_us): - """ - """ - temp_dt_us = np.array([dt_us[i] for i in range(self.input_size * self.pre_step)]) - - return (np.dot(temp_dt_us, np.dot(H, temp_dt_us.reshape(-1, 1))) - np.dot(G.T, temp_dt_us.reshape(-1, 1)))[0] - - # constraint - lb = np.array([-np.inf for _ in range(len(ub))]) - linear_cons = LinearConstraint(A, lb, ub) - - init_dt_us = np.zeros(self.input_size * self.pre_step) - - # constraint - if self.W is not None or self.F is not None : - opt_result = minimize(optimized_func, init_dt_us, constraints=[linear_cons]) - - opt_dt_us = opt_result.x - - opt_u = opt_dt_us[:self.input_size] + self.history_us[-1] - - # save - self.history_us.append(opt_u) - - return opt_u \ No newline at end of file diff --git a/mpc/basic/test_compare_methods.py b/mpc/basic/test_compare_methods.py deleted file mode 100644 index 5ddaea4..0000000 --- a/mpc/basic/test_compare_methods.py +++ /dev/null @@ -1,211 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from mpc_func_with_scipy import MpcController as MpcController_scipy -from mpc_func_with_cvxopt import MpcController as MpcController_cvxopt -from control import matlab - -class FirstOrderSystem(): - """FirstOrderSystemWithStates - - Attributes - ----------- - states : float - system states - A : numpy.ndarray - system matrix - B : numpy.ndarray - control matrix - C : numpy.ndarray - observation matrix - history_state : list - time history of state - """ - def __init__(self, A, B, C, D=None, init_states=None): - """ - Parameters - ----------- - A : numpy.ndarray - system matrix - B : numpy.ndarray - control matrix - C : numpy.ndarray - observation matrix - C : numpy.ndarray - directly matrix - init_state : float, optional - initial state of system default is None - history_xs : list - time history of system states - """ - - self.A = A - self.B = B - self.C = C - - if D is not None: - self.D = D - - self.xs = np.zeros(self.A.shape[0]) - - if init_states is not None: - self.xs = copy.deepcopy(init_states) - - self.history_xs = [init_states] - - def update_state(self, u, dt=0.01): - """calculating input - Parameters - ------------ - u : float - input of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - temp_x = self.xs.reshape(-1, 1) - temp_u = u.reshape(-1, 1) - - # solve Runge-Kutta - k0 = dt * (np.dot(self.A, temp_x) + np.dot(self.B, temp_u)) - k1 = dt * (np.dot(self.A, temp_x + k0/2.) + np.dot(self.B, temp_u)) - k2 = dt * (np.dot(self.A, temp_x + k1/2.) + np.dot(self.B, temp_u)) - k3 = dt * (np.dot(self.A, temp_x + k2) + np.dot(self.B, temp_u)) - - self.xs += ((k0 + 2 * k1 + 2 * k2 + k3) / 6.).flatten() - - # for oylar - # self.xs += k0.flatten() - - # print("xs = {0}".format(self.xs)) - # a = input() - # save - save_states = copy.deepcopy(self.xs) - self.history_xs.append(save_states) - # print(self.history_xs) - -def main(): - dt = 0.05 - simulation_time = 50 # in seconds - iteration_num = int(simulation_time / dt) - - # you must be care about this matrix - # these A and B are for continuos system if you want to use discret system matrix please skip this step - tau = 0.63 - A = np.array([[-1./tau, 0., 0., 0.], - [0., -1./tau, 0., 0.], - [1., 0., 0., 0.], - [0., 1., 0., 0.]]) - B = np.array([[1./tau, 0.], - [0., 1./tau], - [0., 0.], - [0., 0.]]) - - C = np.eye(4) - D = np.zeros((4, 2)) - - # make simulator with coninuous matrix - init_xs = np.array([0., 0., 0., 0.]) - plant_cvxopt = FirstOrderSystem(A, B, C, init_states=init_xs) - plant_scipy = FirstOrderSystem(A, B, C, init_states=init_xs) - - # create system - sysc = matlab.ss(A, B, C, D) - # discrete system - sysd = matlab.c2d(sysc, dt) - - Ad = sysd.A - Bd = sysd.B - - # evaluation function weight - Q = np.diag([1., 1., 10., 10.]) - R = np.diag([0.01, 0.01]) - pre_step = 5 - - # make controller with discreted matrix - # please check the solver, if you want to use the scipy, set the MpcController_scipy - controller_cvxopt = MpcController_cvxopt(Ad, Bd, Q, R, pre_step, - dt_input_upper=np.array([0.25 * dt, 0.25 * dt]), dt_input_lower=np.array([-0.5 * dt, -0.5 * dt]), - input_upper=np.array([1. ,3.]), input_lower=np.array([-1., -3.])) - - controller_scipy = MpcController_scipy(Ad, Bd, Q, R, pre_step, - dt_input_upper=np.array([0.25 * dt, 0.25 * dt]), dt_input_lower=np.array([-0.5 * dt, -0.5 * dt]), - input_upper=np.array([1. ,3.]), input_lower=np.array([-1., -3.])) - - controller_cvxopt.initialize_controller() - controller_scipy.initialize_controller() - - for i in range(iteration_num): - print("simulation time = {0}".format(i)) - reference = np.array([[0., 0., -5., 7.5] for _ in range(pre_step)]).flatten() - - states_cvxopt = plant_cvxopt.xs - states_scipy = plant_scipy.xs - - opt_u_cvxopt = controller_cvxopt.calc_input(states_cvxopt, reference) - opt_u_scipy = controller_scipy.calc_input(states_scipy, reference) - - plant_cvxopt.update_state(opt_u_cvxopt) - plant_scipy.update_state(opt_u_scipy) - - history_states_cvxopt = np.array(plant_cvxopt.history_xs) - history_states_scipy = np.array(plant_scipy.history_xs) - - time_history_fig = plt.figure(dpi=75) - x_fig = time_history_fig.add_subplot(411) - y_fig = time_history_fig.add_subplot(412) - v_x_fig = time_history_fig.add_subplot(413) - v_y_fig = time_history_fig.add_subplot(414) - - v_x_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_cvxopt[:, 0], label="cvxopt") - v_x_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_scipy[:, 0], label="scipy", linestyle="dashdot") - v_x_fig.plot(np.arange(0, simulation_time+0.01, dt), [0. for _ in range(iteration_num+1)], linestyle="dashed") - v_x_fig.set_xlabel("time [s]") - v_x_fig.set_ylabel("v_x") - v_x_fig.legend() - - v_y_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_cvxopt[:, 1], label="cvxopt") - v_y_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_scipy[:, 1], label="scipy", linestyle="dashdot") - v_y_fig.plot(np.arange(0, simulation_time+0.01, dt), [0. for _ in range(iteration_num+1)], linestyle="dashed") - v_y_fig.set_xlabel("time [s]") - v_y_fig.set_ylabel("v_y") - v_y_fig.legend() - - x_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_cvxopt[:, 2], label="cvxopt") - x_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_scipy[:, 2], label="scipy", linestyle="dashdot") - x_fig.plot(np.arange(0, simulation_time+0.01, dt), [-5. for _ in range(iteration_num+1)], linestyle="dashed") - x_fig.set_xlabel("time [s]") - x_fig.set_ylabel("x") - - y_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_cvxopt[:, 3], label ="cvxopt") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), history_states_scipy[:, 3], label="scipy", linestyle="dashdot") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), [7.5 for _ in range(iteration_num+1)], linestyle="dashed") - y_fig.set_xlabel("time [s]") - y_fig.set_ylabel("y") - time_history_fig.tight_layout() - plt.show() - - history_us_cvxopt = np.array(controller_cvxopt.history_us) - history_us_scipy = np.array(controller_scipy.history_us) - - input_history_fig = plt.figure(dpi=75) - u_1_fig = input_history_fig.add_subplot(211) - u_2_fig = input_history_fig.add_subplot(212) - - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), history_us_cvxopt[:, 0], label="cvxopt") - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), history_us_scipy[:, 0], label="scipy", linestyle="dashdot") - u_1_fig.set_xlabel("time [s]") - u_1_fig.set_ylabel("u_x") - u_1_fig.legend() - - u_2_fig.plot(np.arange(0, simulation_time+0.01, dt), history_us_cvxopt[:, 1], label="cvxopt") - u_2_fig.plot(np.arange(0, simulation_time+0.01, dt), history_us_scipy[:, 1], label="scipy", linestyle="dashdot") - u_2_fig.set_xlabel("time [s]") - u_2_fig.set_ylabel("u_y") - u_2_fig.legend() - input_history_fig.tight_layout() - plt.show() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/mpc/extend/README.md b/mpc/extend/README.md deleted file mode 100644 index e926aa5..0000000 --- a/mpc/extend/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Model Predictive Control for Vehicle model -This program is for controlling the vehicle model. -I implemented the steering control for vehicle by using Model Predictive Control. - -# Model -Usually, the vehicle model is expressed by extremely complicated nonlinear equation. -Acoording to reference 1, I used the simple model as shown in following equation. - - - -However, it is still a nonlinear equation. -Therefore, I assume that the car is tracking the reference trajectory. -If we get the assumption, the model can turn to linear model by using the path's curvatures. - - - -and \delta_r denoted - - - -R is the curvatures of the reference trajectory. - -Now we can get the linear state equation and can apply the MPC theory. - -However, you should care that this state euation could be changed during the predict horizon. -I implemented this, so if you know about the detail please go to "IteraticeMPC_func.py" - -# Expected Results - - - - - -# Usage - -``` -$ python main_track.py -``` - -# Reference -- 1. https://qiita.com/taka_horibe/items/47f86e02e2db83b0c570#%E8%BB%8A%E4%B8%A1%E3%81%AE%E8%BB%8C%E9%81%93%E8%BF%BD%E5%BE%93%E5%95%8F%E9%A1%8C%E9%9D%9E%E7%B7%9A%E5%BD%A2%E3%81%AB%E9%81%A9%E7%94%A8%E3%81%99%E3%82%8B (Japanese) diff --git a/mpc/extend/animation.py b/mpc/extend/animation.py deleted file mode 100755 index a085b5a..0000000 --- a/mpc/extend/animation.py +++ /dev/null @@ -1,324 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.animation as ani -import matplotlib.font_manager as fon -import sys -import math - -# default setting of figures -plt.rcParams["mathtext.fontset"] = 'stix' # math fonts -plt.rcParams['xtick.direction'] = 'in' # x axis in -plt.rcParams['ytick.direction'] = 'in' # y axis in -plt.rcParams["font.size"] = 10 -plt.rcParams['axes.linewidth'] = 1.0 # axis line width -plt.rcParams['axes.grid'] = True # make grid - -def coordinate_transformation_in_angle(positions, base_angle): - ''' - Transformation the coordinate in the angle - - Parameters - ------- - positions : numpy.ndarray - this parameter is composed of xs, ys - should have (2, N) shape - base_angle : float [rad] - - Returns - ------- - traslated_positions : numpy.ndarray - the shape is (2, N) - - ''' - if positions.shape[0] != 2: - raise ValueError('the input data should have (2, N)') - - positions = np.array(positions) - positions = positions.reshape(2, -1) - - rot_matrix = [[np.cos(base_angle), np.sin(base_angle)], - [-1*np.sin(base_angle), np.cos(base_angle)]] - - rot_matrix = np.array(rot_matrix) - - translated_positions = np.dot(rot_matrix, positions) - - return translated_positions - -def square_make_with_angles(center_x, center_y, size, angle): - ''' - Create square matrix with angle line matrix(2D) - - Parameters - ------- - center_x : float in meters - the center x position of the square - center_y : float in meters - the center y position of the square - size : float in meters - the square's half-size - angle : float in radians - - Returns - ------- - square xs : numpy.ndarray - lenght is 5 (counterclockwise from right-up) - square ys : numpy.ndarray - length is 5 (counterclockwise from right-up) - angle line xs : numpy.ndarray - angle line ys : numpy.ndarray - ''' - - # start with the up right points - # create point in counterclockwise - square_xys = np.array([[size, 0.5 * size], [-size, 0.5 * size], [-size, -0.5 * size], [size, -0.5 * size], [size, 0.5 * size]]) - trans_points = coordinate_transformation_in_angle(square_xys.T, -angle) # this is inverse type - trans_points += np.array([[center_x], [center_y]]) - - square_xs = trans_points[0, :] - square_ys = trans_points[1, :] - - angle_line_xs = [center_x, center_x + math.cos(angle) * size] - angle_line_ys = [center_y, center_y + math.sin(angle) * size] - - return square_xs, square_ys, np.array(angle_line_xs), np.array(angle_line_ys) - - -def circle_make_with_angles(center_x, center_y, radius, angle): - ''' - Create circle matrix with angle line matrix - - Parameters - ------- - center_x : float - the center x position of the circle - center_y : float - the center y position of the circle - radius : float - angle : float [rad] - - Returns - ------- - circle xs : numpy.ndarray - circle ys : numpy.ndarray - angle line xs : numpy.ndarray - angle line ys : numpy.ndarray - ''' - - point_num = 100 # 分解能 - - circle_xs = [] - circle_ys = [] - - for i in range(point_num + 1): - circle_xs.append(center_x + radius * math.cos(i*2*math.pi/point_num)) - circle_ys.append(center_y + radius * math.sin(i*2*math.pi/point_num)) - - angle_line_xs = [center_x, center_x + math.cos(angle) * radius] - angle_line_ys = [center_y, center_y + math.sin(angle) * radius] - - return np.array(circle_xs), np.array(circle_ys), np.array(angle_line_xs), np.array(angle_line_ys) - - -class AnimDrawer(): - """create animation of path and robot - - Attributes - ------------ - cars : - anim_fig : figure of matplotlib - axis : axis of matplotlib - - """ - def __init__(self, objects): - """ - Parameters - ------------ - objects : list of objects - - Notes - --------- - lead_history_states, lead_history_predict_states, traj_ref, history_traj_ref, history_angle_ref - """ - self.lead_car_history_state = objects[0] - self.lead_car_history_predict_state = objects[1] - self.traj = objects[2] - self.history_traj_ref = objects[3] - self.history_angle_ref = objects[4] - - self.history_xs = [self.lead_car_history_state[:, 0]] - self.history_ys = [self.lead_car_history_state[:, 1]] - self.history_ths = [self.lead_car_history_state[:, 2]] - - # setting up figure - self.anim_fig = plt.figure(dpi=150) - self.axis = self.anim_fig.add_subplot(111) - - # imgs - self.car_imgs = [] - self.traj_imgs = [] - self.predict_imgs = [] - - def draw_anim(self, interval=50): - """draw the animation and save - - Parameteres - ------------- - interval : int, optional - animation's interval time, you should link the sampling time of systems - default is 50 [ms] - """ - self._set_axis() - self._set_img() - - self.skip_num = 1 - frame_num = int((len(self.history_xs[0])-1) / self.skip_num) - - animation = ani.FuncAnimation(self.anim_fig, self._update_anim, interval=interval, frames=frame_num) - - # self.axis.legend() - print('save_animation?') - shuold_save_animation = int(input()) - - if shuold_save_animation: - print('animation_number?') - num = int(input()) - animation.save('animation_{0}.mp4'.format(num), writer='ffmpeg') - # animation.save("Sample.gif", writer = 'imagemagick') # gif保存 - - plt.show() - - def _set_axis(self): - """ initialize the animation axies - """ - # (1) set the axis name - self.axis.set_xlabel(r'$\it{x}$ [m]') - self.axis.set_ylabel(r'$\it{y}$ [m]') - self.axis.set_aspect('equal', adjustable='box') - - LOW_MARGIN = 5 - HIGH_MARGIN = 5 - - self.axis.set_xlim(np.min(self.history_xs) - LOW_MARGIN, np.max(self.history_xs) + HIGH_MARGIN) - self.axis.set_ylim(np.min(self.history_ys) - LOW_MARGIN, np.max(self.history_ys) + HIGH_MARGIN) - - def _set_img(self): - """ initialize the imgs of animation - this private function execute the make initial imgs for animation - """ - # object imgs - obj_color_list = ["k", "k", "m", "m"] - obj_styles = ["solid", "solid", "solid", "solid"] - - for i in range(len(obj_color_list)): - temp_img, = self.axis.plot([], [], color=obj_color_list[i], linestyle=obj_styles[i]) - self.car_imgs.append(temp_img) - - traj_color_list = ["k", "b"] - - for i in range(len(traj_color_list)): - temp_img, = self.axis.plot([],[], color=traj_color_list[i], linestyle="dashed") - self.traj_imgs.append(temp_img) - - temp_img, = self.axis.plot([],[], ".", color="m") - self.traj_imgs.append(temp_img) - - # predict - for _ in range(2 * len(self.history_angle_ref[0])): - temp_img, = self.axis.plot([],[], color="g", linewidth=0.5) # point - # temp_img, = self.axis.plot([],[], ".", color="g", linewidth=0.5) # point - self.predict_imgs.append(temp_img) - - def _update_anim(self, i): - """the update animation - this function should be used in the animation functions - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - - Returns - ----------- - object_imgs : list of img - traj_imgs : list of img - """ - i = int(i * self.skip_num) - - # self._draw_set_axis(i) - self._draw_car(i) - self._draw_traj(i) - # self._draw_prediction(i) - - return self.car_imgs, self.traj_imgs, self.predict_imgs, - - def _draw_set_axis(self, i): - """ - """ - # (2) set the xlim and ylim - LOW_MARGIN = 20 - HIGH_MARGIN = 20 - OVER_LOOK = 50 - self.axis.set_xlim(np.min(self.history_xs[0][i : i + OVER_LOOK]) - LOW_MARGIN, np.max(self.history_xs[0][i : i + OVER_LOOK]) + HIGH_MARGIN) - self.axis.set_ylim(np.min(self.history_ys[0][i : i + OVER_LOOK]) - LOW_MARGIN, np.max(self.history_ys[0][i : i + OVER_LOOK]) + HIGH_MARGIN) - - def _draw_car(self, i): - """ - This private function is just divided thing of - the _update_anim to see the code more clear - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - """ - # cars - object_x, object_y, angle_x, angle_y = square_make_with_angles(self.history_xs[0][i], - self.history_ys[0][i], - 5.0, - self.history_ths[0][i]) - - self.car_imgs[0].set_data([object_x, object_y]) - self.car_imgs[1].set_data([angle_x, angle_y]) - - def _draw_traj(self, i): - """ - This private function is just divided thing of - the _update_anim to see the code more clear - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - """ - # car - self.traj_imgs[0].set_data(self.history_xs[0][:i], self.history_ys[0][:i]) - - # all traj_ref - self.traj_imgs[1].set_data(self.traj[0, :], self.traj[1, :]) - - # traj_ref - # self.traj_imgs[2].set_data(self.history_traj_ref[i][0, :], self.history_traj_ref[i][1, :]) - - def _draw_prediction(self, i): - """draw prediction - - Parameters - ------------ - i : int - time step of the animation - the sampling time should be related to the sampling time of system - """ - - for j in range(0, len(self.history_angle_ref[0]), 4): - fix_j = j * 2 - object_x, object_y, angle_x, angle_y =\ - circle_make_with_angles(self.lead_car_history_predict_state[i][j, 0], - self.lead_car_history_predict_state[i][j, 1], 1., - self.lead_car_history_predict_state[i][j, 2]) - - self.predict_imgs[fix_j].set_data(object_x, object_y) - self.predict_imgs[fix_j + 1].set_data(angle_x, angle_y) \ No newline at end of file diff --git a/mpc/extend/coordinate_trans.py b/mpc/extend/coordinate_trans.py deleted file mode 100755 index 9cfa220..0000000 --- a/mpc/extend/coordinate_trans.py +++ /dev/null @@ -1,112 +0,0 @@ -import math -import numpy as np -import copy - -def coordinate_transformation_in_angle(positions, base_angle): - ''' - Transformation the coordinate in the angle - - Parameters - ------- - positions : numpy.ndarray - this parameter is composed of xs, ys - should have (2, N) shape - base_angle : float [rad] - - Returns - ------- - traslated_positions : numpy.ndarray - the shape is (2, N) - - ''' - if positions.shape[0] != 2: - raise ValueError('the input data should have (2, N)') - - positions = np.array(positions) - positions = positions.reshape(2, -1) - - rot_matrix = [[np.cos(base_angle), np.sin(base_angle)], - [-1*np.sin(base_angle), np.cos(base_angle)]] - - rot_matrix = np.array(rot_matrix) - - translated_positions = np.dot(rot_matrix, positions) - - return translated_positions - -def coordinate_transformation_in_position(positions, base_positions): - ''' - Transformation the coordinate in the positions - - Parameters - ------- - positions : numpy.ndarray - this parameter is composed of xs, ys - should have (2, N) shape - base_positions : numpy.ndarray - this parameter is composed of x, y - shoulg have (2, 1) shape - - Returns - ------- - traslated_positions : numpy.ndarray, shape(2, N) - - ''' - - if positions.shape[0] != 2: - raise ValueError('the input data should have (2, N)') - - positions = np.array(positions) - positions = positions.reshape(2, -1) - base_positions = np.array(base_positions) - base_positions = base_positions.reshape(2, 1) - - translated_positions = positions - base_positions - - return translated_positions - - -def coordinate_transformation_in_matrix_angles(positions, base_angles): - ''' - Transformation the coordinate in the matrix angle - - Parameters - ------- - positions : numpy.ndarray - this parameter is composed of xs, ys - should have (2, N) shape - base_angle : float [rad] - - Returns - ------- - traslated_positions : numpy.ndarray - the shape is (2, N) - - ''' - if positions.shape[0] != 2: - raise ValueError('the input data should have (2, N)') - - positions = np.array(positions) - positions = positions.reshape(2, -1) - translated_positions = np.zeros_like(positions) - - for i in range(len(base_angles)): - rot_matrix = [[np.cos(base_angles[i]), np.sin(base_angles[i])], - [-1*np.sin(base_angles[i]), np.cos(base_angles[i])]] - - rot_matrix = np.array(rot_matrix) - - translated_position = np.dot(rot_matrix, positions[:, i].reshape(2, 1)) - - translated_positions[:, i] = translated_position.flatten() - - return translated_positions.reshape(2, -1) - -# def coordinate_inv_transformation -if __name__ == '__main__': - positions_1 = np.array([[1.0], [2.0]]) - base_angle = 1.25 - - translated_positions_1 = coordinate_transformation_in_angle(positions_1, base_angle) - print(translated_positions_1) - diff --git a/mpc/extend/extended_MPC.py b/mpc/extend/extended_MPC.py deleted file mode 100644 index c365aaf..0000000 --- a/mpc/extend/extended_MPC.py +++ /dev/null @@ -1,306 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from cvxopt import matrix, solvers -import cvxopt -cvxopt.solvers.options['show_progress'] = False # not display - -class IterativeMpcController(): - """ - Attributes - ------------ - Ad_s : list of numpy.ndarray - system matrix - Bd_s : list of numpy.ndarray - input matrix - W_D_s : list of numpy.ndarray - distubance matrix in state equation - Q : numpy.ndarray - evaluation function weight for states - Qs : numpy.ndarray - concatenated evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - Rs : numpy.ndarray - concatenated evaluation function weight for inputs - pre_step : int - prediction step - state_size : int - state size of the plant - input_size : int - input size of the plant - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - """ - def __init__(self, system_model, Q, R, pre_step, initial_input=None, dt_input_upper=None, dt_input_lower=None, input_upper=None, input_lower=None): - """ - Parameters - ------------ - system_model : SystemModel class - Q : numpy.ndarray - evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - pre_step : int - prediction step - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - history_us : list - time history of optimal input us(numpy.ndarray) - """ - self.Ad_s = system_model.Ad_s - self.Bd_s = system_model.Bd_s - self.W_D_s = system_model.W_D_s - self.Q = np.array(Q) - self.R = np.array(R) - self.pre_step = pre_step - - self.Qs = None - self.Rs = None - - self.state_size = self.Ad_s[0].shape[0] - self.input_size = self.Bd_s[0].shape[1] - - self.history_us = [np.zeros(self.input_size)] - - # initial state - if initial_input is not None: - self.history_us = [initial_input] - - # constraints - self.dt_input_lower = dt_input_lower - self.dt_input_upper = dt_input_upper - self.input_upper = input_upper - self.input_lower = input_lower - - # about mpc matrix - self.W = None - self.omega = None - self.F = None - self.f = None - - def initialize_controller(self): - """ - make matrix to calculate optimal control input - """ - A_factorials = [self.Ad_s[0]] - - self.phi_mat = copy.deepcopy(self.Ad_s[0]) - - for i in range(self.pre_step - 1): - temp_mat = np.dot(A_factorials[-1], self.Ad_s[i + 1]) - self.phi_mat = np.vstack((self.phi_mat, temp_mat)) - - A_factorials.append(temp_mat) # after we use this factorials - - # print("phi_mat = \n{0}".format(self.phi_mat)) - - self.gamma_mat = copy.deepcopy(self.Bd_s[0]) - gammma_mat_temp = copy.deepcopy(self.Bd_s[0]) - - for i in range(self.pre_step - 1): - temp_1_mat = np.dot(A_factorials[i], self.Bd_s[i + 1]) - gammma_mat_temp = temp_1_mat + gammma_mat_temp - self.gamma_mat = np.vstack((self.gamma_mat, gammma_mat_temp)) - - # print("gamma_mat = \n{0}".format(self.gamma_mat)) - - self.theta_mat = copy.deepcopy(self.gamma_mat) - - for i in range(self.pre_step - 1): - temp_mat = np.zeros_like(self.gamma_mat) - temp_mat[int((i + 1)*self.state_size): , :] = self.gamma_mat[:-int((i + 1)*self.state_size) , :] - - self.theta_mat = np.hstack((self.theta_mat, temp_mat)) - - # print("theta_mat = \n{0}".format(self.theta_mat)) - - # disturbance - # print("A_factorials_mat = \n{0}".format(A_factorials)) - A_factorials_mat = np.array(A_factorials).reshape(-1, self.state_size) - # print("A_factorials_mat = \n{0}".format(A_factorials_mat)) - - eye = np.eye(self.state_size) - self.dist_mat = np.vstack((eye, A_factorials_mat[:-self.state_size, :])) - base_mat = copy.deepcopy(self.dist_mat) - - # print("base_mat = \n{0}".format(base_mat)) - - for i in range(self.pre_step - 1): - temp_mat = np.zeros_like(A_factorials_mat) - temp_mat[int((i + 1)*self.state_size): , :] = base_mat[:-int((i + 1)*self.state_size) , :] - self.dist_mat = np.hstack((self.dist_mat, temp_mat)) - - # print("dist_mat = \n{0}".format(self.dist_mat)) - - W_Ds = copy.deepcopy(self.W_D_s[0]) - - for i in range(self.pre_step - 1): - W_Ds = np.vstack((W_Ds, self.W_D_s[i + 1])) - - self.dist_mat = np.dot(self.dist_mat, W_Ds) - - # print("dist_mat = \n{0}".format(self.dist_mat)) - - # evaluation function weight - diag_Qs = np.array([np.diag(self.Q) for _ in range(self.pre_step)]) - diag_Rs = np.array([np.diag(self.R) for _ in range(self.pre_step)]) - - self.Qs = np.diag(diag_Qs.flatten()) - self.Rs = np.diag(diag_Rs.flatten()) - - # print("Qs = \n{0}".format(self.Qs)) - # print("Rs = \n{0}".format(self.Rs)) - - # constraints - # about dt U - if self.input_lower is not None: - # initialize - self.F = np.zeros((self.input_size * 2, self.pre_step * self.input_size)) - for i in range(self.input_size): - self.F[i * 2: (i + 1) * 2, i] = np.array([1., -1.]) - temp_F = copy.deepcopy(self.F) - - # print("F = \n{0}".format(self.F)) - - for i in range(self.pre_step - 1): - temp_F = copy.deepcopy(temp_F) - - for j in range(self.input_size): - temp_F[j * 2: (j + 1) * 2, ((i+1) * self.input_size) + j] = np.array([1., -1.]) - - self.F = np.vstack((self.F, temp_F)) - - self.F1 = self.F[:, :self.input_size] - - temp_f = [] - - for i in range(self.input_size): - temp_f.append(-1 * self.input_upper[i]) - temp_f.append(self.input_lower[i]) - - self.f = np.array([temp_f for _ in range(self.pre_step)]).flatten() - - # print("F = \n{0}".format(self.F)) - # print("F1 = \n{0}".format(self.F1)) - # print("f = \n{0}".format(self.f)) - - # about dt_u - if self.dt_input_lower is not None: - self.W = np.zeros((2, self.pre_step * self.input_size)) - self.W[:, 0] = np.array([1., -1.]) - - for i in range(self.pre_step * self.input_size - 1): - temp_W = np.zeros((2, self.pre_step * self.input_size)) - temp_W[:, i+1] = np.array([1., -1.]) - self.W = np.vstack((self.W, temp_W)) - - temp_omega = [] - - for i in range(self.input_size): - temp_omega.append(self.dt_input_upper[i]) - temp_omega.append(-1. * self.dt_input_lower[i]) - - self.omega = np.array([temp_omega for _ in range(self.pre_step)]).flatten() - - # print("W = \n{0}".format(self.W)) - # print("omega = \n{0}".format(self.omega)) - - # about state - print("check the matrix!! if you think rite, plese push enter") - # input() - - def calc_input(self, states, references): - """calculate optimal input - Parameters - ----------- - states : numpy.ndarray, shape(state length, ) - current state of system - references : numpy.ndarray, shape(state length * pre_step, ) - reference of the system, you should set this value as reachable goal - - References - ------------ - opt_u : numpy.ndarray, shape(input_length, ) - optimal input - all_opt_u : numpy.ndarray, shape(PREDICT_STEP, input_length) - """ - temp_1 = np.dot(self.phi_mat, states.reshape(-1, 1)) - temp_2 = np.dot(self.gamma_mat, self.history_us[-1].reshape(-1, 1)) - - error = references.reshape(-1, 1) - temp_1 - temp_2 - self.dist_mat - - G = 2. * np.dot(self.theta_mat.T, np.dot(self.Qs, error)) - - H = np.dot(self.theta_mat.T, np.dot(self.Qs, self.theta_mat)) + self.Rs - - # constraints - A = [] - b = [] - - if self.W is not None: - A.append(self.W) - b.append(self.omega.reshape(-1, 1)) - - if self.F is not None: - b_F = - np.dot(self.F1, self.history_us[-1].reshape(-1, 1)) - self.f.reshape(-1, 1) - A.append(self.F) - b.append(b_F) - - A = np.array(A).reshape(-1, self.input_size * self.pre_step) - - ub = np.array(b).flatten() - - # make cvxpy problem formulation - P = 2*matrix(H) - q = matrix(-1 * G) - A = matrix(A) - b = matrix(ub) - - # constraint - if self.W is not None or self.F is not None : - opt_result = solvers.qp(P, q, G=A, h=b) - - opt_dt_us = list(opt_result['x']) - - opt_u = opt_dt_us[:self.input_size] + self.history_us[-1] - - # calc all predit u - all_opt_u = [copy.deepcopy(opt_u)] - temp_u = copy.deepcopy(opt_u) - - for i in range(1, self.pre_step): - temp_u += opt_dt_us[i * self.input_size: (i + 1) * self.input_size] - all_opt_u.append(copy.deepcopy(temp_u)) - - # save - self.history_us.append(opt_u) - - return opt_u, np.array(all_opt_u) - - def update_system_model(self, system_model): - """update system model - Parameters - ----------- - system_model : SystemModel class - """ - - self.Ad_s = system_model.Ad_s - self.Bd_s = system_model.Bd_s - self.W_D_s = system_model.W_D_s - - self.initialize_controller() diff --git a/mpc/extend/func_curvature.py b/mpc/extend/func_curvature.py deleted file mode 100644 index cdd99e6..0000000 --- a/mpc/extend/func_curvature.py +++ /dev/null @@ -1,182 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math - -import random - -from traj_func import make_sample_traj - -def calc_curvature(points): - """ - Parameters - ----------- - points : numpy.ndarray, shape (2, 3) - these points should follow subseqently - - Returns - ---------- - curvature : float - """ - # Gradient 1 - diff = points[:, 0] - points[:, 1] - Gradient_1 = -1. / (diff[1] / diff[0]) - # Gradient 2 - diff = points[:, 1] - points[:, 2] - Gradient_2 = -1. / (diff[1] / diff[0]) - - # middle point 1 - middle_point_1 = (points[:, 0] + points[:, 1]) / 2. - - # middle point 2 - middle_point_2 = (points[:, 1] + points[:, 2]) / 2. - - # calc center - c_x = (middle_point_1[1] - middle_point_2[1] - middle_point_1[0] * Gradient_1 + middle_point_2[0] * Gradient_2) / (Gradient_2 - Gradient_1) - c_y = middle_point_1[1] - (middle_point_1[0] - c_x) * Gradient_1 - - R = math.sqrt((points[0, 0] - c_x)**2 + (points[1, 0] - c_y)**2) - - """ - plt.scatter(points[0, :], points[1, :]) - plt.scatter(c_x, c_y) - - plot_points_x = [] - plot_points_y = [] - - for theta in np.arange(0, 2*math.pi, 0.01): - plot_points_x.append(math.cos(theta)*R + c_x) - plot_points_y.append(math.sin(theta)*R + c_y) - - plt.plot(plot_points_x, plot_points_y) - - plt.show() - """ - - return 1. / R - -def calc_curvatures(traj_ref, predict_step, num_skip): - """ - Parameters - ----------- - traj_ref : numpy.ndarray, shape (2, N) - these points should follow subseqently - predict_step : int - predict step - num_skip : int - skip_num - - Returns - ---------- - angles : list - curvature : list - """ - - angles = [] - curvatures = [] - - for i in range(predict_step): - # make pairs - points = np.zeros((2, 3)) - - points[:, 0] = traj_ref[:, i] - points[:, 1] = traj_ref[:, i + num_skip] - points[:, 2] = traj_ref[:, i + 2 * num_skip] - - # Gradient 1 - diff = points[:, 0] - points[:, 1] - Gradient_1 = -1. / (diff[1] / diff[0]) - # Gradient 2 - diff = points[:, 1] - points[:, 2] - Gradient_2 = -1. / (diff[1] / diff[0]) - - # middle point 1 - middle_point_1 = (points[:, 0] + points[:, 1]) / 2. - - # middle point 2 - middle_point_2 = (points[:, 1] + points[:, 2]) / 2. - - # calc center - c_x = (middle_point_1[1] - middle_point_2[1] - middle_point_1[0] * Gradient_1 + middle_point_2[0] * Gradient_2) / (Gradient_2 - Gradient_1) - c_y = middle_point_1[1] - (middle_point_1[0] - c_x) * Gradient_1 - - # calc R - R = math.sqrt((points[0, 0] - c_x)**2 + (points[1, 0] - c_y)**2) - - # add - diff = points[:, 2] - points[:, 0] - angles.append(math.atan2(diff[1], diff[0])) - curvatures.append(1. / R) - - # plot - """ - plt.scatter(points[0, :], points[1, :]) - plt.scatter(c_x, c_y) - - plot_points_x = [] - plot_points_y = [] - - for theta in np.arange(0, 2*math.pi, 0.01): - plot_points_x.append(math.cos(theta)*R + c_x) - plot_points_y.append(math.sin(theta)*R + c_y) - - plt.plot(plot_points_x, plot_points_y) - - plot_points_x = [] - plot_points_y = [] - - for x in np.arange(-5, 5, 0.01): - plot_points_x.append(x) - plot_points_y.append(x * math.tan(angles[-1])) - - plt.plot(plot_points_x, plot_points_y) - - plt.xlim(-1, 10) - plt.ylim(-1, 2) - - plt.show() - """ - - return angles, curvatures - -def calc_ideal_vel(traj_ref, dt): - """ - Parameters - ------------ - traj_ref : numpy.ndarray, shape (2, N) - these points should follow subseqently - dt : float - sampling time of system - """ - # end point and start point - diff = traj_ref[:, -1] - traj_ref[:, 0] - distance = np.sqrt(np.sum(diff**2)) - - V = distance / (dt * traj_ref.shape[1]) - - return V - -def main(): - """ - points = np.zeros((2, 3)) - points[:, 0] = np.array([1. + random.random(), random.random()]) - - points[:, 1] = np.array([random.random(), 3 + random.random()]) - - points[:, 2] = np.array([3 + random.random(), -3 + random.random()]) - - calc_cuvature(points) - """ - - traj_ref_xs, traj_ref_ys = make_sample_traj(1000) - traj_ref = np.array([traj_ref_xs, traj_ref_ys]) - - calc_curvatures(traj_ref[:, 10:10 + 15 + 100 * 2], 15, 100) - - -if __name__ == "__main__": - main() - - - - - diff --git a/mpc/extend/main_track.py b/mpc/extend/main_track.py deleted file mode 100644 index f78413f..0000000 --- a/mpc/extend/main_track.py +++ /dev/null @@ -1,464 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -# from mpc_func_with_cvxopt import MpcController as MpcController_cvxopt -from extended_MPC import IterativeMpcController -from animation import AnimDrawer -# from control import matlab -from coordinate_trans import coordinate_transformation_in_angle, coordinate_transformation_in_position -from traj_func import make_sample_traj -from func_curvature import calc_curvatures, calc_ideal_vel - -class WheeledSystem(): - """SampleSystem, this is the simulator - Kinematic model of car - - Attributes - ----------- - xs : numpy.ndarray - system states, [x, y, phi, beta] - history_xs : list - time history of state - tau : float - time constant of tire - FRONT_WHEEL_BASE : float - REAR_WHEEL_BASE : float - predict_xs : - """ - def __init__(self, init_states=None): - """ - Palameters - ----------- - init_state : float, optional, shape(3, ) - initial state of system default is None - """ - self.NUM_STATE = 4 - self.xs = np.zeros(self.NUM_STATE) - - self.tau = 0.01 - - self.FRONT_WHEELE_BASE = 1.0 - self.REAR_WHEELE_BASE = 1.0 - - if init_states is not None: - self.xs = copy.deepcopy(init_states) - - self.history_xs = [init_states] - self.history_predict_xs = [] - - def update_state(self, us, dt=0.01): - """ - Palameters - ------------ - u : numpy.ndarray - inputs of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - k0 = [0.0 for _ in range(self.NUM_STATE)] - k1 = [0.0 for _ in range(self.NUM_STATE)] - k2 = [0.0 for _ in range(self.NUM_STATE)] - k3 = [0.0 for _ in range(self.NUM_STATE)] - - functions = [self._func_x_1, self._func_x_2, self._func_x_3, self._func_x_4] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(self.xs[0], self.xs[1], self.xs[2], self.xs[3], us[0], us[1]) - - for i, func in enumerate(functions): - k1[i] = dt * func(self.xs[0] + k0[0]/2., self.xs[1] + k0[1]/2., self.xs[2] + k0[2]/2., self.xs[3] + k0[3]/2, us[0], us[1]) - - for i, func in enumerate(functions): - k2[i] = dt * func(self.xs[0] + k1[0]/2., self.xs[1] + k1[1]/2., self.xs[2] + k1[2]/2., self.xs[3] + k1[3]/2., us[0], us[1]) - - for i, func in enumerate(functions): - k3[i] = dt * func(self.xs[0] + k2[0], self.xs[1] + k2[1], self.xs[2] + k2[2], self.xs[3] + k2[3], us[0], us[1]) - - self.xs[0] += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - self.xs[1] += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - self.xs[2] += (k0[2] + 2. * k1[2] + 2. * k2[2] + k3[2]) / 6. - self.xs[3] += (k0[3] + 2. * k1[3] + 2. * k2[3] + k3[3]) / 6. - - # save - save_states = copy.deepcopy(self.xs) - self.history_xs.append(save_states) - # print(self.xs) - - def predict_state(self, us, dt=0.01): - """make predict state by using optimal input made by MPC - Paramaters - ----------- - us : array-like, shape(2, N) - optimal input made by MPC - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - - xs = copy.deepcopy(self.xs) - predict_xs = [copy.deepcopy(xs)] - - for i in range(us.shape[1]): - k0 = [0.0 for _ in range(self.NUM_STATE)] - k1 = [0.0 for _ in range(self.NUM_STATE)] - k2 = [0.0 for _ in range(self.NUM_STATE)] - k3 = [0.0 for _ in range(self.NUM_STATE)] - - functions = [self._func_x_1, self._func_x_2, self._func_x_3, self._func_x_4] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(xs[0], xs[1], xs[2], xs[3], us[0, i], us[1, i]) - - for i, func in enumerate(functions): - k1[i] = dt * func(xs[0] + k0[0]/2., xs[1] + k0[1]/2., xs[2] + k0[2]/2., xs[3] + k0[3]/2., us[0, i], us[1, i]) - - for i, func in enumerate(functions): - k2[i] = dt * func(xs[0] + k1[0]/2., xs[1] + k1[1]/2., xs[2] + k1[2]/2., xs[3] + k1[3]/2., us[0, i], us[1, i]) - - for i, func in enumerate(functions): - k3[i] = dt * func(xs[0] + k2[0], xs[1] + k2[1], xs[2] + k2[2], xs[3] + k2[3], us[0, i], us[1, i]) - - xs[0] += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - xs[1] += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - xs[2] += (k0[2] + 2. * k1[2] + 2. * k2[2] + k3[2]) / 6. - xs[3] += (k0[3] + 2. * k1[3] + 2. * k2[3] + k3[3]) / 6. - - predict_xs.append(copy.deepcopy(xs)) - - self.history_predict_xs.append(np.array(predict_xs)) - - def _func_x_1(self, y_1, y_2, y_3, y_4, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - # y_dot = u_1 * math.cos(y_3 + y_4) - y_dot = u_1 * math.cos(y_3) - - return y_dot - - def _func_x_2(self, y_1, y_2, y_3, y_4, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - # y_dot = u_1 * math.sin(y_3 + y_4) - y_dot = u_1 * math.sin(y_3) - - return y_dot - - def _func_x_3(self, y_1, y_2, y_3, y_4, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - # y_dot = u_1 / self.REAR_WHEELE_BASE * math.sin(y_4) - y_dot = u_1 * math.tan(y_4) / (self.REAR_WHEELE_BASE + self.FRONT_WHEELE_BASE) - - return y_dot - - def _func_x_4(self, y_1, y_2, y_3, y_4, u_1, u_2): - """Ad, Bd, W_D, Q, R - ParAd, Bd, W_D, Q, R - ---Ad, Bd, W_D, Q, R - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - # y_dot = math.atan2(self.REAR_WHEELE_BASE * math.tan(u_2) ,self.REAR_WHEELE_BASE + self.FRONT_WHEELE_BASE) - y_dot = - 1. / self.tau * (y_4 - u_2) - - return y_dot - -class SystemModel(): - """ - Attributes - ----------- - WHEEL_BASE : float - wheel base of the car - Ad_s : list - list of system model matrix Ad - Bd_s : list - list of system model matrix Bd - W_D_s : list - list of system model matrix W_D_s - Q : numpy.ndarray - R : numpy.ndarray - """ - def __init__(self, tau = 0.01, dt = 0.01): - """ - Parameters - ----------- - tau : time constant, optional - dt : sampling time, optional - """ - self.dt = dt - self.tau = tau - self.WHEEL_BASE = 2.2 - - self.Ad_s = [] - self.Bd_s = [] - self.W_D_s = [] - - def calc_predict_sytem_model(self, V, curvatures, predict_step): - """ - calc next predict systemo models - V : float - current speed of car - curvatures : list - this is the next curvature's list - predict_step : int - predict step of MPC - """ - for i in range(predict_step): - delta_r = math.atan2(self.WHEEL_BASE, 1. / curvatures[i]) - - A12 = (V / self.WHEEL_BASE) / (math.cos(delta_r)**2) - A22 = (1. - 1. / self.tau * self.dt) - - Ad = np.array([[1., V * self.dt, 0.], - [0., 1., A12 * self.dt], - [0., 0., A22]]) - - Bd = np.array([[0.], [0.], [1. / self.tau]]) * self.dt - - # -v*curvature + v/L*(tan(delta_r)-delta_r*cos_delta_r_squared_inv); - # W_D_0 = V / self.WHEEL_BASE * (delta_r / (math.cos(delta_r)**2) - W_D_0 = -V * curvatures[i] + (V / self.WHEEL_BASE) * (math.tan(delta_r) - delta_r / (math.cos(delta_r)**2)) - - W_D = np.array([[0.], [W_D_0], [0.]]) * self.dt - - self.Ad_s.append(Ad) - self.Bd_s.append(Bd) - self.W_D_s.append(W_D) - - # return self.Ad_s, self.Bd_s, self.W_D_s - -def search_nearest_point(points, base_point): - """ - Parameters - ----------- - points : numpy.ndarray, shape is (2, N) - base_point : numpy.ndarray, shape is (2, 1) - - Returns - ------- - nearest_index : - nearest_point : - """ - distance_mat = np.sqrt(np.sum((points - base_point)**2, axis=0)) - - index_min = np.argmin(distance_mat) - - return index_min, points[:, index_min] - - -def main(): - # parameters - dt = 0.01 - simulation_time = 20 # in seconds - PREDICT_STEP = 30 - iteration_num = int(simulation_time / dt) - - # make simulator with coninuous matrix - init_xs_lead = np.array([0., 0., math.pi/5, 0.]) - lead_car = WheeledSystem(init_states=init_xs_lead) - - # make system model - lead_car_system_model = SystemModel() - - # reference - history_traj_ref = [] - history_angle_ref = [] - traj_ref_xs, traj_ref_ys = make_sample_traj(int(simulation_time/dt)) - traj_ref = np.array([traj_ref_xs, traj_ref_ys]) - - # nearest point - index_min, nearest_point = search_nearest_point(traj_ref, lead_car.xs[:2].reshape(2, 1)) - - # get traj's curvature - NUM_SKIP = 3 - MARGIN = 50 - angles, curvatures = calc_curvatures(traj_ref[:, index_min + MARGIN:index_min + PREDICT_STEP + 2 * NUM_SKIP + MARGIN], PREDICT_STEP, NUM_SKIP) - - # save traj ref - history_traj_ref.append(traj_ref[:, index_min + MARGIN:index_min + PREDICT_STEP + 2 * NUM_SKIP + MARGIN]) - history_angle_ref.append(angles) - - # print(history_traj_ref) - # input() - - # evaluation function weight - Q = np.diag([1e2, 1., 1e3]) - R = np.diag([1e2]) - - # System model update - V = calc_ideal_vel(traj_ref, dt) # in pratical we should calc from the state - lead_car_system_model.calc_predict_sytem_model(V, curvatures, PREDICT_STEP) - - # make controller with discreted matrix - lead_controller = IterativeMpcController(lead_car_system_model, Q, R, PREDICT_STEP, - dt_input_upper=np.array([1 * dt]), dt_input_lower=np.array([-1 * dt]), - input_upper=np.array([1.]), input_lower=np.array([-1.])) - - - # initialize - lead_controller.initialize_controller() - - for i in range(iteration_num): - print("simulation time = {0}".format(i)) - - ## lead - # world traj - lead_states = lead_car.xs - - # nearest point - index_min, nearest_point = search_nearest_point(traj_ref, lead_car.xs[:2].reshape(2, 1)) - - # end check - if len(traj_ref_ys) <= index_min + PREDICT_STEP + 2 * NUM_SKIP + MARGIN: - print("break") - break - - # get traj's curvature - angles, curvatures = calc_curvatures(traj_ref[:, index_min+MARGIN:index_min + PREDICT_STEP + 2 * NUM_SKIP + MARGIN], PREDICT_STEP, NUM_SKIP) - - # save - history_traj_ref.append(traj_ref[:, index_min + MARGIN:index_min + PREDICT_STEP + 2 * NUM_SKIP + MARGIN]) - history_angle_ref.append(angles) - - # System model update - V = calc_ideal_vel(traj_ref, dt) # in pratical we should calc from the state - lead_car_system_model.calc_predict_sytem_model(V, curvatures, PREDICT_STEP) - - # transformation - # car - relative_car_position = coordinate_transformation_in_position(lead_states[:2].reshape(2, 1), nearest_point) - relative_car_position = coordinate_transformation_in_angle(relative_car_position, angles[0]) - - relative_car_angle = lead_states[2] - angles[0] - relative_car_state = np.hstack((relative_car_position[1], relative_car_angle, lead_states[-1])) - - # traj_ref - relative_traj = coordinate_transformation_in_position(traj_ref[:, index_min:index_min + PREDICT_STEP], nearest_point) - relative_traj = coordinate_transformation_in_angle(relative_traj, angles[0]) - relative_ref_angle = np.array(angles) - angles[0] - - # make ref - lead_reference = np.array([[relative_traj[1, -1], relative_ref_angle[-1], 0.] for i in range(PREDICT_STEP)]).flatten() - - print("relative car state = {}".format(relative_car_state)) - print("nearest point = {}".format(nearest_point)) - # input() - - # update system matrix - lead_controller.update_system_model(lead_car_system_model) - - lead_opt_u, all_opt_u = lead_controller.calc_input(relative_car_state, lead_reference) - - lead_opt_u = np.hstack((np.array([V]), lead_opt_u)) - - all_opt_u = np.stack((np.ones(PREDICT_STEP)*V, all_opt_u.flatten())) - - print("opt_u = {}".format(lead_opt_u)) - print("all_opt_u = {}".format(all_opt_u)) - - # predict - lead_car.predict_state(all_opt_u, dt=dt) - - # update - lead_car.update_state(lead_opt_u, dt=dt) - - # print(lead_car.history_predict_xs) - # input() - - # figures and animation - lead_history_states = np.array(lead_car.history_xs) - lead_history_predict_states = lead_car.history_predict_xs - - """ - time_history_fig = plt.figure() - x_fig = time_history_fig.add_subplot(311) - y_fig = time_history_fig.add_subplot(312) - theta_fig = time_history_fig.add_subplot(313) - - car_traj_fig = plt.figure() - traj_fig = car_traj_fig.add_subplot(111) - traj_fig.set_aspect('equal') - - x_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 0], label="lead") - x_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 0], label="follow") - x_fig.set_xlabel("time [s]") - x_fig.set_ylabel("x") - x_fig.legend() - - y_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 1], label="lead") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 1], label="follow") - y_fig.plot(np.arange(0, simulation_time+0.01, dt), [4. for _ in range(iteration_num+1)], linestyle="dashed") - y_fig.set_xlabel("time [s]") - y_fig.set_ylabel("y") - y_fig.legend() - - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_states[:, 2], label="lead") - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_states[:, 2], label="follow") - theta_fig.plot(np.arange(0, simulation_time+0.01, dt), [0. for _ in range(iteration_num+1)], linestyle="dashed") - theta_fig.set_xlabel("time [s]") - theta_fig.set_ylabel("theta") - theta_fig.legend() - - time_history_fig.tight_layout() - - traj_fig.plot(lead_history_states[:, 0], lead_history_states[:, 1], label="lead") - traj_fig.plot(follow_history_states[:, 0], follow_history_states[:, 1], label="follow") - traj_fig.set_xlabel("x") - traj_fig.set_ylabel("y") - traj_fig.legend() - plt.show() - - lead_history_us = np.array(lead_controller.history_us) - follow_history_us = np.array(follow_controller.history_us) - input_history_fig = plt.figure() - u_1_fig = input_history_fig.add_subplot(111) - - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), lead_history_us[:, 0], label="lead") - u_1_fig.plot(np.arange(0, simulation_time+0.01, dt), follow_history_us[:, 0], label="follow") - u_1_fig.set_xlabel("time [s]") - u_1_fig.set_ylabel("u_omega") - - input_history_fig.tight_layout() - plt.show() - """ - - animdrawer = AnimDrawer([lead_history_states, lead_history_predict_states, traj_ref, history_traj_ref, history_angle_ref]) - animdrawer.draw_anim() - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/mpc/extend/mpc_func_with_cvxopt.py b/mpc/extend/mpc_func_with_cvxopt.py deleted file mode 100644 index a46d2d2..0000000 --- a/mpc/extend/mpc_func_with_cvxopt.py +++ /dev/null @@ -1,304 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -from cvxopt import matrix, solvers - -class MpcController(): - """ - Attributes - ------------ - A : numpy.ndarray - system matrix - B : numpy.ndarray - input matrix - W_D : numpy.ndarray - distubance matrix in state equation - Q : numpy.ndarray - evaluation function weight for states - Qs : numpy.ndarray - concatenated evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - Rs : numpy.ndarray - concatenated evaluation function weight for inputs - pre_step : int - prediction step - state_size : int - state size of the plant - input_size : int - input size of the plant - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - """ - def __init__(self, A, B, W_D, Q, R, pre_step, initial_input=None, dt_input_upper=None, dt_input_lower=None, input_upper=None, input_lower=None): - """ - Parameters - ------------ - A : numpy.ndarray - system matrix - B : numpy.ndarray - input matrix - W_D : numpy.ndarray - distubance matrix in state equation - Q : numpy.ndarray - evaluation function weight for states - R : numpy.ndarray - evaluation function weight for inputs - pre_step : int - prediction step - dt_input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - dt_input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input dt, default is None - input_upper : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - input_lower : numpy.ndarray, shape(input_size, ), optional - constraints of input, default is None - history_us : list - time history of optimal input us(numpy.ndarray) - """ - self.A = np.array(A) - self.B = np.array(B) - self.W_D = np.array(W_D) - self.Q = np.array(Q) - self.R = np.array(R) - self.pre_step = pre_step - - self.Qs = None - self.Rs = None - - self.state_size = self.A.shape[0] - self.input_size = self.B.shape[1] - - self.history_us = [np.zeros(self.input_size)] - - # initial state - if initial_input is not None: - self.history_us = [initial_input] - - # constraints - self.dt_input_lower = dt_input_lower - self.dt_input_upper = dt_input_upper - self.input_upper = input_upper - self.input_lower = input_lower - - # about mpc matrix - self.W = None - self.omega = None - self.F = None - self.f = None - - def initialize_controller(self): - """ - make matrix to calculate optimal control input - - """ - A_factorials = [self.A] - - self.phi_mat = copy.deepcopy(self.A) - - for _ in range(self.pre_step - 1): - temp_mat = np.dot(A_factorials[-1], self.A) - self.phi_mat = np.vstack((self.phi_mat, temp_mat)) - - A_factorials.append(temp_mat) # after we use this factorials - - print("phi_mat = \n{0}".format(self.phi_mat)) - - self.gamma_mat = copy.deepcopy(self.B) - gammma_mat_temp = copy.deepcopy(self.B) - - for i in range(self.pre_step - 1): - temp_1_mat = np.dot(A_factorials[i], self.B) - gammma_mat_temp = temp_1_mat + gammma_mat_temp - self.gamma_mat = np.vstack((self.gamma_mat, gammma_mat_temp)) - - print("gamma_mat = \n{0}".format(self.gamma_mat)) - - self.theta_mat = copy.deepcopy(self.gamma_mat) - - for i in range(self.pre_step - 1): - temp_mat = np.zeros_like(self.gamma_mat) - temp_mat[int((i + 1)*self.state_size): , :] = self.gamma_mat[:-int((i + 1)*self.state_size) , :] - - self.theta_mat = np.hstack((self.theta_mat, temp_mat)) - - print("theta_mat = \n{0}".format(self.theta_mat)) - - # disturbance - print("A_factorials_mat = \n{0}".format(A_factorials)) - A_factorials_mat = np.array(A_factorials).reshape(-1, self.state_size) - print("A_factorials_mat = \n{0}".format(A_factorials_mat)) - - eye = np.eye(self.state_size) - self.dist_mat = np.vstack((eye, A_factorials_mat[:-self.state_size, :])) - base_mat = copy.deepcopy(self.dist_mat) - - print("base_mat = \n{0}".format(base_mat)) - - for i in range(self.pre_step - 1): - temp_mat = np.zeros_like(A_factorials_mat) - temp_mat[int((i + 1)*self.state_size): , :] = base_mat[:-int((i + 1)*self.state_size) , :] - self.dist_mat = np.hstack((self.dist_mat, temp_mat)) - - print("dist_mat = \n{0}".format(self.dist_mat)) - - W_Ds = copy.deepcopy(self.W_D) - - for _ in range(self.pre_step - 1): - W_Ds = np.vstack((W_Ds, self.W_D)) - - self.dist_mat = np.dot(self.dist_mat, W_Ds) - - print("dist_mat = \n{0}".format(self.dist_mat)) - - # evaluation function weight - diag_Qs = np.array([np.diag(self.Q) for _ in range(self.pre_step)]) - diag_Rs = np.array([np.diag(self.R) for _ in range(self.pre_step)]) - - self.Qs = np.diag(diag_Qs.flatten()) - self.Rs = np.diag(diag_Rs.flatten()) - - print("Qs = \n{0}".format(self.Qs)) - print("Rs = \n{0}".format(self.Rs)) - - # constraints - # about dt U - if self.input_lower is not None: - # initialize - self.F = np.zeros((self.input_size * 2, self.pre_step * self.input_size)) - for i in range(self.input_size): - self.F[i * 2: (i + 1) * 2, i] = np.array([1., -1.]) - temp_F = copy.deepcopy(self.F) - - print("F = \n{0}".format(self.F)) - - for i in range(self.pre_step - 1): - temp_F = copy.deepcopy(temp_F) - - for j in range(self.input_size): - temp_F[j * 2: (j + 1) * 2, ((i+1) * self.input_size) + j] = np.array([1., -1.]) - - self.F = np.vstack((self.F, temp_F)) - - self.F1 = self.F[:, :self.input_size] - - temp_f = [] - - for i in range(self.input_size): - temp_f.append(-1 * self.input_upper[i]) - temp_f.append(self.input_lower[i]) - - self.f = np.array([temp_f for _ in range(self.pre_step)]).flatten() - - print("F = \n{0}".format(self.F)) - print("F1 = \n{0}".format(self.F1)) - print("f = \n{0}".format(self.f)) - - # about dt_u - if self.dt_input_lower is not None: - self.W = np.zeros((2, self.pre_step * self.input_size)) - self.W[:, 0] = np.array([1., -1.]) - - for i in range(self.pre_step * self.input_size - 1): - temp_W = np.zeros((2, self.pre_step * self.input_size)) - temp_W[:, i+1] = np.array([1., -1.]) - self.W = np.vstack((self.W, temp_W)) - - temp_omega = [] - - for i in range(self.input_size): - temp_omega.append(self.dt_input_upper[i]) - temp_omega.append(-1. * self.dt_input_lower[i]) - - self.omega = np.array([temp_omega for _ in range(self.pre_step)]).flatten() - - print("W = \n{0}".format(self.W)) - print("omega = \n{0}".format(self.omega)) - - # about state - print("check the matrix!! if you think rite, plese push enter") - # input() - - def calc_input(self, states, references): - """calculate optimal input - Parameters - ----------- - states : numpy.ndarray, shape(state length, ) - current state of system - references : numpy.ndarray, shape(state length * pre_step, ) - reference of the system, you should set this value as reachable goal - - References - ------------ - opt_input : numpy.ndarray, shape(input_length, ) - optimal input - """ - temp_1 = np.dot(self.phi_mat, states.reshape(-1, 1)) - temp_2 = np.dot(self.gamma_mat, self.history_us[-1].reshape(-1, 1)) - - error = references.reshape(-1, 1) - temp_1 - temp_2 - self.dist_mat - - G = 2. * np.dot(self.theta_mat.T, np.dot(self.Qs, error)) - - H = np.dot(self.theta_mat.T, np.dot(self.Qs, self.theta_mat)) + self.Rs - - # constraints - A = [] - b = [] - - if self.W is not None: - A.append(self.W) - b.append(self.omega.reshape(-1, 1)) - - if self.F is not None: - b_F = - np.dot(self.F1, self.history_us[-1].reshape(-1, 1)) - self.f.reshape(-1, 1) - A.append(self.F) - b.append(b_F) - - A = np.array(A).reshape(-1, self.input_size * self.pre_step) - - ub = np.array(b).flatten() - - # make cvxpy problem formulation - P = 2*matrix(H) - q = matrix(-1 * G) - A = matrix(A) - b = matrix(ub) - - # constraint - if self.W is not None or self.F is not None : - opt_result = solvers.qp(P, q, G=A, h=b) - - opt_dt_us = list(opt_result['x']) - - opt_u = opt_dt_us[:self.input_size] + self.history_us[-1] - - # save - self.history_us.append(opt_u) - - return opt_u - - def update_system_model(self, A, B, W_D): - """update system model - A : numpy.ndarray - system matrix - B : numpy.ndarray - input matrix - W_D : numpy.ndarray - distubance matrix in state equation - """ - - self.A = A - self.B = B - self.W_D = W_D - - self.initialize_controller() diff --git a/mpc/extend/traj_func.py b/mpc/extend/traj_func.py deleted file mode 100644 index 8f5ce18..0000000 --- a/mpc/extend/traj_func.py +++ /dev/null @@ -1,31 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math - -def make_sample_traj(NUM, dt=0.01, a=30.): - """ - make sample trajectory - Parameters - ------------ - NUM : int - dt : float - a : float - - Returns - ---------- - traj_xs : list - traj_ys : list - """ - DELAY = 2. - traj_xs = [] - traj_ys = [] - - for i in range(NUM): - traj_xs.append(i * 0.1) - traj_ys.append(a * math.sin(dt * i / DELAY)) - - plt.plot(traj_xs, traj_ys) - plt.show() - - return traj_xs, traj_ys - diff --git a/nmpc/cgmres/README.md b/nmpc/cgmres/README.md deleted file mode 100644 index a46901c..0000000 --- a/nmpc/cgmres/README.md +++ /dev/null @@ -1,79 +0,0 @@ -# CGMRES method of Nonlinear Model Predictive Control -This program is about Continuous gmres method for NMPC -Although usually we have to calculate the partial differential of optimal matrix, it could be really complicated. -By using CGMRES, we can pass the calculating step and get the optimal input quickly. - -# Problem Formulation - -- **example** - -- model - - - -- evaluation function - - - - -- **two wheeled model** - -- model - - - -- evaluation function - - - - -if you want to see more detail about this methods, you should go https://qiita.com/MENDY/items/4108190a579395053924. -However, it is written in Japanese - -# Expected Results - -- example - -![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/261584/3347fb3c-3fce-63fe-36d5-8a7bb053531a.png) - -- two wheeled model - -- trajectory - -![image.png](https://qiita-image-store.s3.amazonaws.com/0/261584/8e39150d-24ed-af13-51f0-0ca97cb5f5ec.png) - -- time history - -![Figure_1.png](https://qiita-image-store.s3.amazonaws.com/0/261584/e67794f3-e8ef-5162-ea84-eb6adefd4241.png) -![Figure_2.png](https://qiita-image-store.s3.amazonaws.com/0/261584/d74fa06d-2eae-5aea-4a33-6c63e284341e.png) - -# Usage - -- for example - -``` -$ python main_example.py -``` - -- for two wheeled - -``` -$ python main_two_wheeled.py -``` - -# Requirement - -- python3.5 or more -- numpy -- matplotlib - -# Reference -I`m sorry that main references are written in Japanese - -- main (commentary article) (Japanse) https://qiita.com/MENDY/items/4108190a579395053924 - -- Ohtsuka, T., & Fujii, H. A. (1997). Real-time Optimization Algorithm for Nonlinear Receding-horizon Control. Automatica, 33(6), 1147–1154. https://doi.org/10.1016/S0005-1098(97)00005-8 - -- 非線形最適制御入門(コロナ社) - -- 実時間最適化による制御の実応用(コロナ社) \ No newline at end of file diff --git a/nmpc/cgmres/main_example.py b/nmpc/cgmres/main_example.py deleted file mode 100644 index 696742c..0000000 --- a/nmpc/cgmres/main_example.py +++ /dev/null @@ -1,645 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math - -class SampleSystem(): - """SampleSystem, this is the simulator - Attributes - ----------- - x_1 : float - system state 1 - x_2 : float - system state 2 - history_x_1 : list - time history of system state 1 (x_1) - history_x_2 : list - time history of system state 2 (x_2) - """ - def __init__(self, init_x_1=0., init_x_2=0.): - """ - Palameters - ----------- - init_x_1 : float, optional - initial value of x_1, default is 0. - init_x_2 : float, optional - initial value of x_2, default is 0. - """ - self.x_1 = init_x_1 - self.x_2 = init_x_2 - self.history_x_1 = [init_x_1] - self.history_x_2 = [init_x_2] - - def update_state(self, u, dt=0.01): - """ - Palameters - ------------ - u : float - input of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - # for theta 1, theta 1 dot, theta 2, theta 2 dot - k0 = [0.0 for _ in range(2)] - k1 = [0.0 for _ in range(2)] - k2 = [0.0 for _ in range(2)] - k3 = [0.0 for _ in range(2)] - - functions = [self._func_x_1, self._func_x_2] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(self.x_1, self.x_2, u) - - for i, func in enumerate(functions): - k1[i] = dt * func(self.x_1 + k0[0]/2., self.x_2 + k0[1]/2., u) - - for i, func in enumerate(functions): - k2[i] = dt * func(self.x_1 + k1[0]/2., self.x_2 + k1[1]/2., u) - - for i, func in enumerate(functions): - k3[i] = dt * func(self.x_1 + k2[0], self.x_2 + k2[1], u) - - self.x_1 += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - self.x_2 += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - - # save - self.history_x_1.append(self.x_1) - self.history_x_2.append(self.x_2) - - def _func_x_1(self, y_1, y_2, u): - """ - Parameters - ------------ - y_1 : float - y_2 : float - u : float - system input - """ - y_dot = y_2 - return y_dot - - def _func_x_2(self, y_1, y_2, u): - """ - Parameters - ------------ - y_1 : float - y_2 : float - u : float - system input - """ - y_dot = (1. - y_1**2 - y_2**2) * y_2 - y_1 + u - return y_dot - - -class NMPCSimulatorSystem(): - """SimulatorSystem for nmpc, this is the simulator of nmpc - the reason why I seperate the real simulator and nmpc's simulator is sometimes the modeling error, disturbance can include in real simulator - Attributes - ----------- - None - - """ - def __init__(self): - """ - Parameters - ----------- - None - """ - pass - - def calc_predict_and_adjoint_state(self, x_1, x_2, us, N, dt): - """main - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - us : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - """ - - x_1s, x_2s = self._calc_predict_states(x_1, x_2, us, N, dt) # by usin state equation - lam_1s, lam_2s = self._calc_adjoint_states(x_1s, x_2s, us, N, dt) # by using adjoint equation - - return x_1s, x_2s, lam_1s, lam_2s - - def _calc_predict_states(self, x_1, x_2, us, N, dt): - """ - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - us : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - """ - # initial state - x_1s = [x_1] - x_2s = [x_2] - - for i in range(N): - temp_x_1, temp_x_2 = self._predict_state_with_oylar(x_1s[i], x_2s[i], us[i], dt) - x_1s.append(temp_x_1) - x_2s.append(temp_x_2) - - return x_1s, x_2s - - def _calc_adjoint_states(self, x_1s, x_2s, us, N, dt): - """ - Parameters - ------------ - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - us : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - """ - # final state - # final_state_func - lam_1s = [x_1s[-1]] - lam_2s = [x_2s[-1]] - - for i in range(N-1, 0, -1): - temp_lam_1, temp_lam_2 = self._adjoint_state_with_oylar(x_1s[i], x_2s[i], lam_1s[0] ,lam_2s[0], us[i], dt) - lam_1s.insert(0, temp_lam_1) - lam_2s.insert(0, temp_lam_2) - - return lam_1s, lam_2s - - def final_state_func(self): - """this func usually need - """ - pass - - def _predict_state_with_oylar(self, x_1, x_2, u, dt): - """in this case this function is the same as simulator - Parameters - ------------ - x_1 : float - system state - x_2 : float - system state - u : float - system input - dt : float in seconds - sampling time - Returns - -------- - next_x_1 : float - next state, x_1 calculated by using state equation - next_x_2 : float - next state, x_2 calculated by using state equation - """ - k0 = [0. for _ in range(2)] - - functions = [self.func_x_1, self.func_x_2] - - for i, func in enumerate(functions): - k0[i] = dt * func(x_1, x_2, u) - - next_x_1 = x_1 + k0[0] - next_x_2 = x_2 + k0[1] - - return next_x_1, next_x_2 - - def func_x_1(self, y_1, y_2, u): - """calculating \dot{x_1} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - u : float - means system input - Returns - --------- - y_dot : float - means \dot{x_1} - """ - y_dot = y_2 - return y_dot - - def func_x_2(self, y_1, y_2, u): - """calculating \dot{x_2} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - u : float - means system input - Returns - --------- - y_dot : float - means \dot{x_2} - """ - y_dot = (1. - y_1**2 - y_2**2) * y_2 - y_1 + u - return y_dot - - def _adjoint_state_with_oylar(self, x_1, x_2, lam_1, lam_2, u, dt): - """ - Parameters - ------------ - x_1 : float - system state - x_2 : float - system state - lam_1 : float - adjoint state - lam_2 : float - adjoint state - u : float - system input - dt : float in seconds - sampling time - Returns - -------- - pre_lam_1 : float - pre, 1 step before lam_1 calculated by using adjoint equation - pre_lam_2 : float - pre, 1 step before lam_2 calculated by using adjoint equation - """ - k0 = [0. for _ in range(2)] - - functions = [self._func_lam_1, self._func_lam_2] - - for i, func in enumerate(functions): - k0[i] = dt * func(x_1, x_2, lam_1, lam_2, u) - - pre_lam_1 = lam_1 + k0[0] - pre_lam_2 = lam_2 + k0[1] - - return pre_lam_1, pre_lam_2 - - def _func_lam_1(self, y_1, y_2, y_3, y_4, u): - """calculating -\dot{lam_1} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - y_3 : float - means lam_1 - y_4 : float - means lam_2 - u : float - means system input - Returns - --------- - y_dot : float - means -\dot{lam_1} - """ - y_dot = y_1 - (2. * y_1 * y_2 + 1.) * y_4 - return y_dot - - def _func_lam_2(self, y_1, y_2, y_3, y_4, u): - """calculating -\dot{lam_2} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - y_3 : float - means lam_1 - y_4 : float - means lam_2 - u : float - means system input - Returns - --------- - y_dot : float - means -\dot{lam_2} - """ - y_dot = y_2 + y_3 + (-3. * (y_2**2) - y_1**2 + 1. ) * y_4 - return y_dot - -class NMPCController_with_CGMRES(): - """ - Attributes - ------------ - zeta : float - gain of optimal answer stability - ht : float - update value of NMPC this should be decided by zeta - tf : float - predict time - alpha : float - gain of predict time - N : int - predicte step, discritize value - threshold : float - cgmres's threshold value - input_num : int - system input length, this should include dummy u and constraint variables - max_iteration : int - decide by the solved matrix size - simulator : NMPCSimulatorSystem class - us : list of float - estimated optimal system input - dummy_us : list of float - estimated dummy input - raws : list of float - estimated constraint variable - history_u : list of float - time history of actual system input - history_dummy_u : list of float - time history of actual dummy u - history_raw : list of float - time history of actual raw - history_f : list of float - time history of error of optimal - """ - def __init__(self): - """ - Parameters - ----------- - None - """ - # parameters - self.zeta = 100. # 安定化ゲイン - self.ht = 0.01 # 差分近似の幅 - self.tf = 1. # 最終時間 - self.alpha = 0.5 # 時間の上昇ゲイン - self.N = 10 # 分割数 - self.threshold = 0.001 # break値 - - self.input_num = 3 # dummy, 制約条件に対するuにも合わせた入力の数 - self.max_iteration = self.input_num * self.N - - # simulator - self.simulator = NMPCSimulatorSystem() - - # initial - self.us = np.zeros(self.N) - self.dummy_us = np.ones(self.N) * 0.49 - self.raws = np.ones(self.N) * 0.011 - - # for fig - self.history_u = [] - self.history_dummy_u = [] - self.history_raw = [] - self.history_f = [] - - def calc_input(self, x_1, x_2, time): - """ - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - time : float in seconds - now time - Returns - -------- - us : list of float - estimated optimal system input - """ - # calculating sampling time - dt = self.tf * (1. - np.exp(-self.alpha * time)) / float(self.N) - - # x_dot - x_1_dot = self.simulator.func_x_1(x_1, x_2, self.us[0]) - x_2_dot = self.simulator.func_x_2(x_1, x_2, self.us[0]) - - dx_1 = x_1_dot * self.ht - dx_2 = x_2_dot * self.ht - - x_1s, x_2s, lam_1s, lam_2s = self.simulator.calc_predict_and_adjoint_state(x_1 + dx_1, x_2 + dx_2, self.us, self.N, dt) - - # Fxt - Fxt = self._calc_f(x_1s, x_2s, lam_1s, lam_2s, self.us, self.dummy_us, - self.raws, self.N, dt) - - # F - x_1s, x_2s, lam_1s, lam_2s = self.simulator.calc_predict_and_adjoint_state(x_1, x_2, self.us, self.N, dt) - - F = self._calc_f(x_1s, x_2s, lam_1s, lam_2s, self.us, self.dummy_us, - self.raws, self.N, dt) - - right = -self.zeta * F - ((Fxt - F) / self.ht) - - du = self.us * self.ht - ddummy_u = self.dummy_us * self.ht - draw = self.raws * self.ht - - x_1s, x_2s, lam_1s, lam_2s = self.simulator.calc_predict_and_adjoint_state(x_1 + dx_1, x_2 + dx_2, self.us + du, self.N, dt) - - Fuxt = self._calc_f(x_1s, x_2s, lam_1s, lam_2s, self.us + du, self.dummy_us + ddummy_u, - self.raws + draw, self.N, dt) - - left = ((Fuxt - Fxt) / self.ht) - - # calculationg cgmres - r0 = right - left - r0_norm = np.linalg.norm(r0) - - vs = np.zeros((self.max_iteration, self.max_iteration + 1)) # 数×iterarion回数 - - vs[:, 0] = r0 / r0_norm # 最初の基底を算出 - - hs = np.zeros((self.max_iteration + 1, self.max_iteration + 1)) - - e = np.zeros((self.max_iteration + 1, 1)) # in this case the state is 3(u and dummy_u) - e[0] = 1. - - for i in range(self.max_iteration): - du = vs[::3, i] * self.ht - ddummy_u = vs[1::3, i] * self.ht - draw = vs[2::3, i] * self.ht - - x_1s, x_2s, lam_1s, lam_2s = self.simulator.calc_predict_and_adjoint_state(x_1 + dx_1, x_2 + dx_2, self.us + du, self.N, dt) - - Fuxt = self._calc_f(x_1s, x_2s, lam_1s, lam_2s, self.us + du, self.dummy_us + ddummy_u, - self.raws + draw, self.N, dt) - - Av = (( Fuxt - Fxt) / self.ht) - - sum_Av = np.zeros(self.max_iteration) - - for j in range(i + 1): # グラムシュミットの直交化法です、和を取って差分を取って算出します - hs[j, i] = np.dot(Av, vs[:, j]) - sum_Av = sum_Av + hs[j, i] * vs[:, j] - - v_est = Av - sum_Av - - hs[i+1, i] = np.linalg.norm(v_est) - - vs[:, i+1] = v_est / hs[i+1, i] - - inv_hs = np.linalg.pinv(hs[:i+1, :i]) # この辺は教科書(実時間の方)にのっています - ys = np.dot(inv_hs, r0_norm * e[:i+1]) - - judge_value = r0_norm * e[:i+1] - np.dot(hs[:i+1, :i], ys[:i]) - - if np.linalg.norm(judge_value) < self.threshold or i == self.max_iteration-1: - update_value = np.dot(vs[:, :i-1], ys_pre[:i-1]).flatten() - du_new = du + update_value[::3] - ddummy_u_new = ddummy_u + update_value[1::3] - draw_new = draw + update_value[2::3] - break - - ys_pre = ys - - # update - self.us += du_new * self.ht - self.dummy_us += ddummy_u_new * self.ht - self.raws += draw_new * self.ht - - x_1s, x_2s, lam_1s, lam_2s = self.simulator.calc_predict_and_adjoint_state(x_1, x_2, self.us, self.N, dt) - - F = self._calc_f(x_1s, x_2s, lam_1s, lam_2s, self.us, self.dummy_us, - self.raws, self.N, dt) - - print("check F = {0}".format(np.linalg.norm(F))) - - # for save - self.history_f.append(np.linalg.norm(F)) - self.history_u.append(self.us[0]) - self.history_dummy_u.append(self.dummy_us[0]) - self.history_raw.append(self.raws[0]) - - return self.us - - def _calc_f(self, x_1s, x_2s, lam_1s, lam_2s, us, dummy_us, raws, N, dt): - """ - Parameters - ------------ - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - us : list of float - estimated optimal system input - dummy_us : list of float - estimated dummy input - raws : list of float - estimated constraint variable - N : int - predict time step - dt : float - sampling time of system - """ - F = [] - - for i in range(N): - F.append(us[i] + lam_2s[i] + 2. * raws[i] * us[i]) - F.append(-0.01 + 2. * raws[i] * dummy_us[i]) - F.append(us[i]**2 + dummy_us[i]**2 - 0.5**2) - - return np.array(F) - -def main(): - # simulation time - dt = 0.01 - iteration_time = 20. - iteration_num = int(iteration_time/dt) - - # plant - plant_system = SampleSystem(init_x_1=2., init_x_2=0.) - - # controller - controller = NMPCController_with_CGMRES() - - # for i in range(iteration_num) - for i in range(1, iteration_num): - time = float(i) * dt - x_1 = plant_system.x_1 - x_2 = plant_system.x_2 - # make input - us = controller.calc_input(x_1, x_2, time) - # update state - plant_system.update_state(us[0]) - - # figure - fig = plt.figure() - - x_1_fig = fig.add_subplot(321) - x_2_fig = fig.add_subplot(322) - u_fig = fig.add_subplot(323) - dummy_fig = fig.add_subplot(324) - raw_fig = fig.add_subplot(325) - f_fig = fig.add_subplot(326) - - x_1_fig.plot(np.arange(iteration_num)*dt, plant_system.history_x_1) - x_1_fig.set_xlabel("time [s]") - x_1_fig.set_ylabel("x_1") - - x_2_fig.plot(np.arange(iteration_num)*dt, plant_system.history_x_2) - x_2_fig.set_xlabel("time [s]") - x_2_fig.set_ylabel("x_2") - - u_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_u) - u_fig.set_xlabel("time [s]") - u_fig.set_ylabel("u") - - dummy_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_dummy_u) - dummy_fig.set_xlabel("time [s]") - dummy_fig.set_ylabel("dummy u") - - raw_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_raw) - raw_fig.set_xlabel("time [s]") - raw_fig.set_ylabel("raw") - - f_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_f) - f_fig.set_xlabel("time [s]") - f_fig.set_ylabel("optimal error") - - fig.tight_layout() - - plt.show() - - -if __name__ == "__main__": - main() - - - diff --git a/nmpc/cgmres/main_two_wheeled.py b/nmpc/cgmres/main_two_wheeled.py deleted file mode 100644 index febf11d..0000000 --- a/nmpc/cgmres/main_two_wheeled.py +++ /dev/null @@ -1,893 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math - -class TwoWheeledSystem(): - """SampleSystem, this is the simulator - Attributes - ----------- - x_1 : float - system state 1 - x_2 : float - system state 2 - history_x_1 : list - time history of system state 1 (x_1) - history_x_2 : list - time history of system state 2 (x_2) - """ - def __init__(self, init_x_1=0., init_x_2=0., init_x_3=0.): - """ - Palameters - ----------- - init_x_1 : float, optional - initial value of x_1, default is 0. - init_x_2 : float, optional - initial value of x_2, default is 0. - init_x_3 : float, optional - initial value of x_3, default is 0. - """ - self.x_1 = init_x_1 - self.x_2 = init_x_2 - self.x_3 = init_x_3 - self.history_x_1 = [init_x_1] - self.history_x_2 = [init_x_2] - self.history_x_3 = [init_x_3] - - def update_state(self, u_1, u_2, dt=0.01): - """ - Palameters - ------------ - u_1 : float - input of system in some cases this means the reference, u_velocity - u_2 : float - input of system in some cases this means the reference, u_omega - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - # for theta 1, theta 1 dot, theta 2, theta 2 dot - k0 = [0.0 for _ in range(3)] - k1 = [0.0 for _ in range(3)] - k2 = [0.0 for _ in range(3)] - k3 = [0.0 for _ in range(3)] - - functions = [self._func_x_1, self._func_x_2, self._func_x_3] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(self.x_1, self.x_2, self.x_3, u_1, u_2) - - for i, func in enumerate(functions): - k1[i] = dt * func(self.x_1 + k0[0]/2., self.x_2 + k0[1]/2., self.x_3 + k0[2]/2., u_1, u_2) - - for i, func in enumerate(functions): - k2[i] = dt * func(self.x_1 + k1[0]/2., self.x_2 + k1[1]/2., self.x_3 + k1[2]/2., u_1, u_2) - - for i, func in enumerate(functions): - k3[i] = dt * func(self.x_1 + k2[0], self.x_2 + k2[1], self.x_3 + k2[2], u_1, u_2) - - self.x_1 += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - self.x_2 += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - self.x_3 += (k0[2] + 2. * k1[2] + 2. * k2[2] + k3[2]) / 6. - - # save - self.history_x_1.append(self.x_1) - self.history_x_2.append(self.x_2) - self.history_x_3.append(self.x_3) - - def _func_x_1(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.cos(y_3) * u_1 - return y_dot - - def _func_x_2(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.sin(y_3) * u_1 - return y_dot - - def _func_x_3(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = u_2 - return y_dot - -class NMPCSimulatorSystem(): - """SimulatorSystem for nmpc, this is the simulator of nmpc - the reason why I seperate the real simulator and nmpc's simulator is sometimes the modeling error, disturbance can include in real simulator - Attributes - ----------- - None - - """ - def __init__(self): - """ - Parameters - ----------- - None - """ - pass - - def calc_predict_and_adjoint_state(self, x_1, x_2, x_3, u_1s, u_2s, N, dt): - """main - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - x_3 : float - current state - u_1s : list of float - estimated optimal input Us for N steps - u_2s : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - x_3s : list of float - predicted x_3s for N steps - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - lam_3s : list of float - adjoint state of x_3s, lam_3s for N steps - """ - x_1s, x_2s, x_3s = self._calc_predict_states(x_1, x_2, x_3, u_1s, u_2s, N, dt) # by usin state equation - lam_1s, lam_2s, lam_3s = self._calc_adjoint_states(x_1s, x_2s, x_3s, u_1s, u_2s, N, dt) # by using adjoint equation - - return x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s - - def _calc_predict_states(self, x_1, x_2, x_3, u_1s, u_2s, N, dt): - """ - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - x_3 : float - current state - u_1s : list of float - estimated optimal input Us for N steps - u_2s : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - x_3s : list of float - predicted x_3s for N steps - """ - # initial state - x_1s = [x_1] - x_2s = [x_2] - x_3s = [x_3] - - for i in range(N): - temp_x_1, temp_x_2, temp_x_3 = self._predict_state_with_oylar(x_1s[i], x_2s[i], x_3s[i], u_1s[i], u_2s[i], dt) - x_1s.append(temp_x_1) - x_2s.append(temp_x_2) - x_3s.append(temp_x_3) - - return x_1s, x_2s, x_3s - - def _calc_adjoint_states(self, x_1s, x_2s, x_3s, u_1s, u_2s, N, dt): - """ - Parameters - ------------ - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - x_3s : list of float - predicted x_3s for N steps - u_1s : list of float - estimated optimal input Us for N steps - u_2s : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - lam_3s : list of float - adjoint state of x_2s, lam_2s for N steps - """ - # final state - # final_state_func - lam_1s = [x_1s[-1]] - lam_2s = [x_2s[-1]] - lam_3s = [x_3s[-1]] - - for i in range(N-1, 0, -1): - temp_lam_1, temp_lam_2, temp_lam_3 = self._adjoint_state_with_oylar(x_1s[i], x_2s[i], x_3s[i], lam_1s[0] ,lam_2s[0], lam_3s[0], u_1s[i], u_2s[i], dt) - lam_1s.insert(0, temp_lam_1) - lam_2s.insert(0, temp_lam_2) - lam_3s.insert(0, temp_lam_3) - - return lam_1s, lam_2s, lam_3s - - def final_state_func(self): - """this func usually need - """ - pass - - def _predict_state_with_oylar(self, x_1, x_2, x_3, u_1, u_2, dt): - """in this case this function is the same as simulator - Parameters - ------------ - x_1 : float - system state - x_2 : float - system state - x_3 : float - system state - u_1 : float - system input - u_2 : float - system input - dt : float in seconds - sampling time - Returns - -------- - next_x_1 : float - next state, x_1 calculated by using state equation - next_x_2 : float - next state, x_2 calculated by using state equation - next_x_3 : float - next state, x_3 calculated by using state equation - """ - k0 = [0. for _ in range(3)] - - functions = [self.func_x_1, self.func_x_2, self.func_x_3] - - for i, func in enumerate(functions): - k0[i] = dt * func(x_1, x_2, x_3, u_1, u_2) - - next_x_1 = x_1 + k0[0] - next_x_2 = x_2 + k0[1] - next_x_3 = x_3 + k0[2] - - return next_x_1, next_x_2, next_x_3 - - def func_x_1(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.cos(y_3) * u_1 - return y_dot - - def func_x_2(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = math.sin(y_3) * u_1 - return y_dot - - def func_x_3(self, y_1, y_2, y_3, u_1, u_2): - """ - Parameters - ------------ - y_1 : float - y_2 : float - y_3 : float - u_1 : float - system input - u_2 : float - system input - """ - y_dot = u_2 - return y_dot - - def _adjoint_state_with_oylar(self, x_1, x_2, x_3, lam_1, lam_2, lam_3, u_1, u_2, dt): - """ - Parameters - ------------ - x_1 : float - system state - x_2 : float - system state - x_3 : float - system state - lam_1 : float - adjoint state - lam_2 : float - adjoint state - lam_3 : float - adjoint state - u_1 : float - system input - u_2 : float - system input - dt : float in seconds - sampling time - Returns - -------- - pre_lam_1 : float - pre, 1 step before lam_1 calculated by using adjoint equation - pre_lam_2 : float - pre, 1 step before lam_2 calculated by using adjoint equation - pre_lam_3 : float - pre, 1 step before lam_3 calculated by using adjoint equation - """ - k0 = [0. for _ in range(3)] - - functions = [self._func_lam_1, self._func_lam_2, self._func_lam_3] - - for i, func in enumerate(functions): - k0[i] = dt * func(x_1, x_2, x_3, lam_1, lam_2, lam_3, u_1, u_2) - - pre_lam_1 = lam_1 + k0[0] - pre_lam_2 = lam_2 + k0[1] - pre_lam_3 = lam_3 + k0[2] - - return pre_lam_1, pre_lam_2, pre_lam_3 - - def _func_lam_1(self, y_1, y_2, y_3, y_4, y_5, y_6, u_1, u_2): - """calculating -\dot{lam_1} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - y_3 : float - means x_3 - y_4 : float - means lam_1 - y_5 : float - means lam_2 - y_6 : float - means lam_3 - u_1 : float - means system input - u_2 : float - means system input - Returns - --------- - y_dot : float - means -\dot{lam_1} - """ - y_dot = 0. - return y_dot - - def _func_lam_2(self, y_1, y_2, y_3, y_4, y_5, y_6, u_1, u_2): - """calculating -\dot{lam_2} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - y_3 : float - means x_3 - y_4 : float - means lam_1 - y_5 : float - means lam_2 - y_6 : float - means lam_3 - u_1 : float - means system input - u_2 : float - means system input - Returns - --------- - y_dot : float - means -\dot{lam_2} - """ - y_dot = 0. - return y_dot - - def _func_lam_3(self, y_1, y_2, y_3, y_4, y_5, y_6, u_1, u_2): - """calculating -\dot{lam_3} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - y_3 : float - means x_3 - y_4 : float - means lam_1 - y_5 : float - means lam_2 - y_6 : float - means lam_3 - u_1 : float - means system input - u_2 : float - means system input - Returns - --------- - y_dot : float - means -\dot{lam_3} - """ - y_dot = - y_4 * math.sin(y_3) * u_1 + y_5 * math.cos(y_3) * u_1 - return y_dot - -class NMPCController_with_CGMRES(): - """ - Attributes - ------------ - zeta : float - gain of optimal answer stability - ht : float - update value of NMPC this should be decided by zeta - tf : float - predict time - alpha : float - gain of predict time - N : int - predicte step, discritize value - threshold : float - cgmres's threshold value - input_num : int - system input length, this should include dummy u and constraint variables - max_iteration : int - decide by the solved matrix size - simulator : NMPCSimulatorSystem class - u_1s : list of float - estimated optimal system input - u_2s : list of float - estimated optimal system input - dummy_u_1s : list of float - estimated dummy input - dummy_u_2s : list of float - estimated dummy input - raw_1s : list of float - estimated constraint variable - raw_2s : list of float - estimated constraint variable - history_u_1 : list of float - time history of actual system input - history_u_2 : list of float - time history of actual system input - history_dummy_u_1 : list of float - time history of actual dummy u_1 - history_dummy_u_2 : list of float - time history of actual dummy u_2 - history_raw_1 : list of float - time history of actual raw_1 - history_raw_2 : list of float - time history of actual raw_2 - history_f : list of float - time history of error of optimal - """ - def __init__(self): - """ - Parameters - ----------- - None - """ - # parameters - self.zeta = 100. # 安定化ゲイン - self.ht = 0.01 # 差分近似の幅 - self.tf = 1. # 最終時間 - self.alpha = 0.5 # 時間の上昇ゲイン - self.N = 10 # 分割数 - self.threshold = 0.001 # break値 - - self.input_num = 6 # dummy, 制約条件に対するuにも合わせた入力の数 - self.max_iteration = self.input_num * self.N - - # simulator - self.simulator = NMPCSimulatorSystem() - - # initial - self.u_1s = np.ones(self.N) * 1. - self.u_2s = np.ones(self.N) * 0.1 - self.dummy_u_1s = np.ones(self.N) * 0.1 - self.dummy_u_2s = np.ones(self.N) * 2.5 - self.raw_1s = np.ones(self.N) * 0.8 - self.raw_2s = np.ones(self.N) * 0.8 - - # for fig - self.history_u_1 = [] - self.history_u_2 = [] - self.history_dummy_u_1 = [] - self.history_dummy_u_2 = [] - self.history_raw_1 = [] - self.history_raw_2 = [] - self.history_f = [] - - def calc_input(self, x_1, x_2, x_3, time): - """ - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - x_3 : float - current state - time : float in seconds - now time - Returns - -------- - u_1s : list of float - estimated optimal system input - u_2s : list of float - estimated optimal system input - """ - # calculating sampling time - dt = self.tf * (1. - np.exp(-self.alpha * time)) / float(self.N) - - # x_dot - x_1_dot = self.simulator.func_x_1(x_1, x_2, x_3, self.u_1s[0], self.u_2s[0]) - x_2_dot = self.simulator.func_x_2(x_1, x_2, x_3, self.u_1s[0], self.u_2s[0]) - x_3_dot = self.simulator.func_x_3(x_1, x_2, x_3, self.u_1s[0], self.u_2s[0]) - - dx_1 = x_1_dot * self.ht - dx_2 = x_2_dot * self.ht - dx_3 = x_3_dot * self.ht - - x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s = self.simulator.calc_predict_and_adjoint_state(x_1 + dx_1, x_2 + dx_2, x_3 + dx_3, self.u_1s, self.u_2s, self.N, dt) - - # Fxt - Fxt = self._calc_f(x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s, self.u_1s, self.u_2s, self.dummy_u_1s, self.dummy_u_2s, - self.raw_1s, self.raw_2s, self.N, dt) - - # F - x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s = self.simulator.calc_predict_and_adjoint_state(x_1, x_2, x_3, self.u_1s, self.u_2s, self.N, dt) - - F = self._calc_f(x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s, self.u_1s, self.u_2s, self.dummy_u_1s, self.dummy_u_2s, - self.raw_1s, self.raw_2s, self.N, dt) - - right = -self.zeta * F - ((Fxt - F) / self.ht) - - du_1 = self.u_1s * self.ht - du_2 = self.u_2s * self.ht - ddummy_u_1 = self.dummy_u_1s * self.ht - ddummy_u_2 = self.dummy_u_2s * self.ht - draw_1 = self.raw_1s * self.ht - draw_2 = self.raw_2s * self.ht - - x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s = self.simulator.calc_predict_and_adjoint_state(x_1 + dx_1, x_2 + dx_2, x_3 + dx_3, self.u_1s + du_1, self.u_2s + du_2, self.N, dt) - - Fuxt = self._calc_f(x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s, self.u_1s + du_1, self.u_2s + du_2, self.dummy_u_1s + ddummy_u_1, self.dummy_u_2s + ddummy_u_2, - self.raw_1s + draw_1, self.raw_2s + draw_2, self.N, dt) - - left = ((Fuxt - Fxt) / self.ht) - - # calculationg cgmres - r0 = right - left - r0_norm = np.linalg.norm(r0) - - vs = np.zeros((self.max_iteration, self.max_iteration + 1)) # 数×iterarion回数 - - vs[:, 0] = r0 / r0_norm # 最初の基底を算出 - - hs = np.zeros((self.max_iteration + 1, self.max_iteration + 1)) - - e = np.zeros((self.max_iteration + 1, 1)) # in this case the state is 3(u and dummy_u) - e[0] = 1. - - for i in range(self.max_iteration): - du_1 = vs[::self.input_num, i] * self.ht - du_2 = vs[1::self.input_num, i] * self.ht - ddummy_u_1 = vs[2::self.input_num, i] * self.ht - ddummy_u_2 = vs[3::self.input_num, i] * self.ht - draw_1 = vs[4::self.input_num, i] * self.ht - draw_2 = vs[5::self.input_num, i] * self.ht - - x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s = self.simulator.calc_predict_and_adjoint_state(x_1 + dx_1, x_2 + dx_2, x_3 + dx_3, self.u_1s + du_1, self.u_2s + du_2, self.N, dt) - - Fuxt = self._calc_f(x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s, self.u_1s + du_1, self.u_2s + du_2, self.dummy_u_1s + ddummy_u_1, self.dummy_u_2s + ddummy_u_2, - self.raw_1s + draw_1, self.raw_2s + draw_2, self.N, dt) - - Av = (( Fuxt - Fxt) / self.ht) - - sum_Av = np.zeros(self.max_iteration) - - for j in range(i + 1): # グラムシュミットの直交化法です、和を取って差分を取って算出します - hs[j, i] = np.dot(Av, vs[:, j]) - sum_Av = sum_Av + hs[j, i] * vs[:, j] - - v_est = Av - sum_Av - - hs[i+1, i] = np.linalg.norm(v_est) - - vs[:, i+1] = v_est / hs[i+1, i] - - inv_hs = np.linalg.pinv(hs[:i+1, :i]) # この辺は教科書(実時間の方)にのっています - ys = np.dot(inv_hs, r0_norm * e[:i+1]) - - judge_value = r0_norm * e[:i+1] - np.dot(hs[:i+1, :i], ys[:i]) - - if np.linalg.norm(judge_value) < self.threshold or i == self.max_iteration-1: - update_value = np.dot(vs[:, :i-1], ys_pre[:i-1]).flatten() - du_1_new = du_1 + update_value[::self.input_num] - du_2_new = du_2 + update_value[1::self.input_num] - ddummy_u_1_new = ddummy_u_1 + update_value[2::self.input_num] - ddummy_u_2_new = ddummy_u_2 + update_value[3::self.input_num] - draw_1_new = draw_1 + update_value[4::self.input_num] - draw_2_new = draw_2 + update_value[5::self.input_num] - break - - ys_pre = ys - - # update - self.u_1s += du_1_new * self.ht - self.u_2s += du_2_new * self.ht - self.dummy_u_1s += ddummy_u_1_new * self.ht - self.dummy_u_2s += ddummy_u_2_new * self.ht - self.raw_1s += draw_1_new * self.ht - self.raw_2s += draw_2_new * self.ht - - x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s = self.simulator.calc_predict_and_adjoint_state(x_1, x_2, x_3, self.u_1s, self.u_2s, self.N, dt) - - F = self._calc_f(x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s, self.u_1s, self.u_2s, self.dummy_u_1s, self.dummy_u_2s, - self.raw_1s, self.raw_2s, self.N, dt) - - print("check F = {0}".format(np.linalg.norm(F))) - - # for save - self.history_f.append(np.linalg.norm(F)) - self.history_u_1.append(self.u_1s[0]) - self.history_u_2.append(self.u_2s[0]) - self.history_dummy_u_1.append(self.dummy_u_1s[0]) - self.history_dummy_u_2.append(self.dummy_u_2s[0]) - self.history_raw_1.append(self.raw_1s[0]) - self.history_raw_2.append(self.raw_2s[0]) - - return self.u_1s, self.u_2s - - def _calc_f(self, x_1s, x_2s, x_3s, lam_1s, lam_2s, lam_3s, u_1s, u_2s, dummy_u_1s, dummy_u_2s, raw_1s, raw_2s, N, dt): - """ - Parameters - ------------ - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - x_3s : list of float - predicted x_3s for N steps - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - lam_3s : list of float - adjoint state of x_2s, lam_3s for N steps - u_1s : list of float - estimated optimal system input - u_2s : list of float - estimated optimal system input - dummy_u_1s : list of float - estimated dummy input - dummy_u_2s : list of float - estimated dummy input - raw_1s : list of float - estimated constraint variable - raw_2s : list of float - estimated constraint variable - N : int - predict time step - dt : float - sampling time of system - """ - F = [] - - for i in range(N): - F.append(u_1s[i] + lam_1s[i] * math.cos(x_3s[i]) + lam_2s[i] * math.sin(x_3s[i]) + 2 * raw_1s[i] * u_1s[i]) - F.append(u_2s[i] + lam_3s[i] + 2 * raw_2s[i] * u_2s[i]) - F.append(-0.01 + 2. * raw_1s[i] * dummy_u_1s[i]) - F.append(-0.01 + 2. * raw_2s[i] * dummy_u_2s[i]) - F.append(u_1s[i]**2 + dummy_u_1s[i]**2 - 1.**2) - F.append(u_2s[i]**2 + dummy_u_2s[i]**2 - 1.5**2) - - return np.array(F) - -def circle_make_with_angles(center_x, center_y, radius, angle): - ''' - Create circle matrix with angle line matrix - - Parameters - ------- - center_x : float - the center x position of the circle - center_y : float - the center y position of the circle - radius : float - angle : float [rad] - - Returns - ------- - circle xs : numpy.ndarray - circle ys : numpy.ndarray - angle line xs : numpy.ndarray - angle line ys : numpy.ndarray - ''' - - point_num = 100 # 分解能 - - circle_xs = [] - circle_ys = [] - - for i in range(point_num + 1): - circle_xs.append(center_x + radius * math.cos(i*2*math.pi/point_num)) - circle_ys.append(center_y + radius * math.sin(i*2*math.pi/point_num)) - - angle_line_xs = [center_x, center_x + math.cos(angle) * radius] - angle_line_ys = [center_y, center_y + math.sin(angle) * radius] - - return np.array(circle_xs), np.array(circle_ys), np.array(angle_line_xs), np.array(angle_line_ys) - -def main(): - # simulation time - dt = 0.01 - iteration_time = 15. - iteration_num = int(iteration_time/dt) - - # plant - plant_system = TwoWheeledSystem(init_x_1=-4.5, init_x_2=1.5, init_x_3=0.25) - - # controller - controller = NMPCController_with_CGMRES() - - # for i in range(iteration_num) - for i in range(1, iteration_num): - time = float(i) * dt - x_1 = plant_system.x_1 - x_2 = plant_system.x_2 - x_3 = plant_system.x_3 - # make input - u_1s, u_2s = controller.calc_input(x_1, x_2, x_3, time) - # update state - plant_system.update_state(u_1s[0], u_2s[0]) - - # figure - # time history - fig_p = plt.figure() - fig_u = plt.figure() - fig_f = plt.figure() - - # traj - fig_t = plt.figure() - fig_traj = fig_t.add_subplot(111) - fig_traj.set_aspect('equal') - - x_1_fig = fig_p.add_subplot(311) - x_2_fig = fig_p.add_subplot(312) - x_3_fig = fig_p.add_subplot(313) - - u_1_fig = fig_u.add_subplot(411) - u_2_fig = fig_u.add_subplot(412) - dummy_1_fig = fig_u.add_subplot(413) - dummy_2_fig = fig_u.add_subplot(414) - - raw_1_fig = fig_f.add_subplot(311) - raw_2_fig = fig_f.add_subplot(312) - f_fig = fig_f.add_subplot(313) - - x_1_fig.plot(np.arange(iteration_num)*dt, plant_system.history_x_1) - x_1_fig.set_xlabel("time [s]") - x_1_fig.set_ylabel("x_1") - - x_2_fig.plot(np.arange(iteration_num)*dt, plant_system.history_x_2) - x_2_fig.set_xlabel("time [s]") - x_2_fig.set_ylabel("x_2") - - x_3_fig.plot(np.arange(iteration_num)*dt, plant_system.history_x_3) - x_3_fig.set_xlabel("time [s]") - x_3_fig.set_ylabel("x_3") - - u_1_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_u_1) - u_1_fig.set_xlabel("time [s]") - u_1_fig.set_ylabel("u_v") - - u_2_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_u_2) - u_2_fig.set_xlabel("time [s]") - u_2_fig.set_ylabel("u_omega") - - dummy_1_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_dummy_u_1) - dummy_1_fig.set_xlabel("time [s]") - dummy_1_fig.set_ylabel("dummy u_1") - - dummy_2_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_dummy_u_2) - dummy_2_fig.set_xlabel("time [s]") - dummy_2_fig.set_ylabel("dummy u_2") - - raw_1_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_raw_1) - raw_1_fig.set_xlabel("time [s]") - raw_1_fig.set_ylabel("raw_1") - - raw_2_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_raw_2) - raw_2_fig.set_xlabel("time [s]") - raw_2_fig.set_ylabel("raw_2") - - f_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_f) - f_fig.set_xlabel("time [s]") - f_fig.set_ylabel("optimal error") - - fig_traj.plot(plant_system.history_x_1, plant_system.history_x_2, color="b", linestyle="dashed") - fig_traj.set_xlabel("x [m]") - fig_traj.set_ylabel("y [m]") - - write_obj_num = 5 - count_num = int(iteration_num / write_obj_num) - - for i in np.arange(0, iteration_num, count_num): - obj_xs, obj_ys, obj_line_xs, obj_line_ys = circle_make_with_angles(plant_system.history_x_1[i], plant_system.history_x_2[i], 0.5, plant_system.history_x_3[i]) - fig_traj.plot(obj_xs, obj_ys, color="k") - fig_traj.plot(obj_line_xs, obj_line_ys, color="k") - - fig_p.tight_layout() - fig_u.tight_layout() - fig_f.tight_layout() - - plt.show() - - -if __name__ == "__main__": - main() - - - diff --git a/nmpc/newton/README.md b/nmpc/newton/README.md deleted file mode 100644 index 94f24c9..0000000 --- a/nmpc/newton/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# Newton method of Nonlinear Model Predictive Control -This program is about NMPC with newton method. -Usually we have to calculate the partial differential of optimal matrix. -In this program, in stead of using any paticular methods to calculate the partial differential of optimal matrix, I used numerical differentiation. -Therefore, I believe that it easy to understand and extend your model. - -# Problem Formulation - -- **example** - -- model - - - -- evaluation function - -To consider the constraints of input u, I introduced dummy input. - - - - -- **two wheeled model** - -coming soon ! - -# Expected Results - -- example - - - -you can confirm that the my method could consider the constraints of input. - -- two wheeled model - -coming soon ! - -# Usage - -- for example - -``` -$ python main_example.py -``` - -- for two wheeled - -coming soon ! - -# Requirement - -- python3.5 or more -- numpy -- matplotlib - -# Reference -I`m sorry that main references are written in Japanese - -- main (commentary article) (Japanse) https://qiita.com/MENDY/items/4108190a579395053924 - -- Ohtsuka, T., & Fujii, H. A. (1997). Real-time Optimization Algorithm for Nonlinear Receding-horizon Control. Automatica, 33(6), 1147–1154. https://doi.org/10.1016/S0005-1098(97)00005-8 - -- 非線形最適制御入門(コロナ社) - -- 実時間最適化による制御の実応用(コロナ社) diff --git a/nmpc/newton/main_example.py b/nmpc/newton/main_example.py deleted file mode 100644 index 4ab4a4c..0000000 --- a/nmpc/newton/main_example.py +++ /dev/null @@ -1,657 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -import math -import copy - -class SampleSystem(): - """SampleSystem, this is the simulator - Attributes - ----------- - x_1 : float - system state 1 - x_2 : float - system state 2 - history_x_1 : list - time history of system state 1 (x_1) - history_x_2 : list - time history of system state 2 (x_2) - """ - def __init__(self, init_x_1=0., init_x_2=0.): - """ - Palameters - ----------- - init_x_1 : float, optional - initial value of x_1, default is 0. - init_x_2 : float, optional - initial value of x_2, default is 0. - """ - self.x_1 = init_x_1 - self.x_2 = init_x_2 - self.history_x_1 = [init_x_1] - self.history_x_2 = [init_x_2] - - def update_state(self, u, dt=0.01): - """ - Palameters - ------------ - u : float - input of system in some cases this means the reference - dt : float in seconds, optional - sampling time of simulation, default is 0.01 [s] - """ - # for theta 1, theta 1 dot, theta 2, theta 2 dot - k0 = [0.0 for _ in range(2)] - k1 = [0.0 for _ in range(2)] - k2 = [0.0 for _ in range(2)] - k3 = [0.0 for _ in range(2)] - - functions = [self._func_x_1, self._func_x_2] - - # solve Runge-Kutta - for i, func in enumerate(functions): - k0[i] = dt * func(self.x_1, self.x_2, u) - - for i, func in enumerate(functions): - k1[i] = dt * func(self.x_1 + k0[0]/2., self.x_2 + k0[1]/2., u) - - for i, func in enumerate(functions): - k2[i] = dt * func(self.x_1 + k1[0]/2., self.x_2 + k1[1]/2., u) - - for i, func in enumerate(functions): - k3[i] = dt * func(self.x_1 + k2[0], self.x_2 + k2[1], u) - - self.x_1 += (k0[0] + 2. * k1[0] + 2. * k2[0] + k3[0]) / 6. - self.x_2 += (k0[1] + 2. * k1[1] + 2. * k2[1] + k3[1]) / 6. - - # save - self.history_x_1.append(copy.deepcopy(self.x_1)) - self.history_x_2.append(copy.deepcopy(self.x_2)) - - def _func_x_1(self, y_1, y_2, u): - """ - Parameters - ------------ - y_1 : float - y_2 : float - u : float - system input - """ - y_dot = y_2 - return y_dot - - def _func_x_2(self, y_1, y_2, u): - """ - Parameters - ------------ - y_1 : float - y_2 : float - u : float - system input - """ - y_dot = (1. - y_1**2 - y_2**2) * y_2 - y_1 + u - return y_dot - - -class NMPCSimulatorSystem(): - """SimulatorSystem for nmpc, this is the simulator of nmpc - the reason why I seperate the real simulator and nmpc's simulator is sometimes the modeling error, disturbance can include in real simulator - Attributes - ----------- - - """ - def __init__(self): - """ - Parameters - ----------- - None - """ - pass - - def calc_predict_and_adjoint_state(self, x_1, x_2, us, N, dt): - """main - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - us : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - """ - - x_1s, x_2s = self._calc_predict_states(x_1, x_2, us, N, dt) # by usin state equation - lam_1s, lam_2s = self._calc_adjoint_states(x_1s, x_2s, us, N, dt) # by using adjoint equation - - return x_1s, x_2s, lam_1s, lam_2s - - def _calc_predict_states(self, x_1, x_2, us, N, dt): - """ - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - us : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - """ - # initial state - x_1s = np.zeros(N+1) - x_2s = np.zeros(N+1) - - # input initial state - x_1s[0] = x_1 - x_2s[0] = x_2 - - for i in range(N): - temp_x_1, temp_x_2 = self._predict_state_with_oylar(x_1s[i], x_2s[i], us[i], dt) - x_1s[i+1] = temp_x_1 - x_2s[i+1] = temp_x_2 - - return x_1s, x_2s - - def _calc_adjoint_states(self, x_1s, x_2s, us, N, dt): - """ - Parameters - ------------ - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - us : list of float - estimated optimal input Us for N steps - N : int - predict step - dt : float - sampling time - - Returns - -------- - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - """ - # final state - # final_state_func - lam_1s = np.zeros(N) - lam_2s = np.zeros(N) - - # input final state - lam_1s[-1] = x_1s[-1] - lam_2s[-1] = x_2s[-1] - - for i in range(N-1, 0, -1): - temp_lam_1, temp_lam_2 = self._adjoint_state_with_oylar(x_1s[i], x_2s[i], lam_1s[i] ,lam_2s[i], us[i], dt) - lam_1s[i-1] = temp_lam_1 - lam_2s[i-1] = temp_lam_2 - - return lam_1s, lam_2s - - def final_state_func(self): - """this func usually need - """ - pass - - def _predict_state_with_oylar(self, x_1, x_2, u, dt): - """in this case this function is the same as simulator - Parameters - ------------ - x_1 : float - system state - x_2 : float - system state - u : float - system input - dt : float in seconds - sampling time - Returns - -------- - next_x_1 : float - next state, x_1 calculated by using state equation - next_x_2 : float - next state, x_2 calculated by using state equation - """ - k0 = [0. for _ in range(2)] - - functions = [self.func_x_1, self.func_x_2] - - for i, func in enumerate(functions): - k0[i] = dt * func(x_1, x_2, u) - - next_x_1 = x_1 + k0[0] - next_x_2 = x_2 + k0[1] - - return next_x_1, next_x_2 - - def func_x_1(self, y_1, y_2, u): - """calculating \dot{x_1} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - u : float - means system input - Returns - --------- - y_dot : float - means \dot{x_1} - """ - y_dot = y_2 - return y_dot - - def func_x_2(self, y_1, y_2, u): - """calculating \dot{x_2} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - u : float - means system input - Returns - --------- - y_dot : float - means \dot{x_2} - """ - y_dot = (1. - y_1**2 - y_2**2) * y_2 - y_1 + u - return y_dot - - def _adjoint_state_with_oylar(self, x_1, x_2, lam_1, lam_2, u, dt): - """ - Parameters - ------------ - x_1 : float - system state - x_2 : float - system state - lam_1 : float - adjoint state - lam_2 : float - adjoint state - u : float - system input - dt : float in seconds - sampling time - Returns - -------- - pre_lam_1 : float - pre, 1 step before lam_1 calculated by using adjoint equation - pre_lam_2 : float - pre, 1 step before lam_2 calculated by using adjoint equation - """ - k0 = [0. for _ in range(2)] - - functions = [self._func_lam_1, self._func_lam_2] - - for i, func in enumerate(functions): - k0[i] = dt * func(x_1, x_2, lam_1, lam_2, u) - - pre_lam_1 = lam_1 + k0[0] - pre_lam_2 = lam_2 + k0[1] - - return pre_lam_1, pre_lam_2 - - def _func_lam_1(self, y_1, y_2, y_3, y_4, u): - """calculating -\dot{lam_1} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - y_3 : float - means lam_1 - y_4 : float - means lam_2 - u : float - means system input - Returns - --------- - y_dot : float - means -\dot{lam_1} - """ - y_dot = y_1 - (2. * y_1 * y_2 + 1.) * y_4 - return y_dot - - def _func_lam_2(self, y_1, y_2, y_3, y_4, u): - """calculating -\dot{lam_2} - Parameters - ------------ - y_1 : float - means x_1 - y_2 : float - means x_2 - y_3 : float - means lam_1 - y_4 : float - means lam_2 - u : float - means system input - Returns - --------- - y_dot : float - means -\dot{lam_2} - """ - y_dot = y_2 + y_3 + (-3. * (y_2**2) - y_1**2 + 1. ) * y_4 - return y_dot - - -def calc_numerical_gradient(forward_prop, grad_f, all_us, shape, input_size): - """ - Parameters - ------------ - forward_prop : function - forward prop - grad_f : function - gradient function - all_us : numpy.ndarray, shape(pred_len, input_size*3) - all inputs including with dummy input - shape : tuple - shape of Jacobian - input_size : int - input size of system - - Returns - --------- - grad : numpy.ndarray, shape is the same as shape - results of numercial gradient of the input - - References - ----------- - - oreilly japan 0 から作るdeeplearning - https://github.com/oreilly-japan/deep-learning-from-scratch/blob/master/common/gradient.py - """ - h = 1e-3 # 0.01 - grad = np.zeros(shape) - grad_idx = 0 - - it = np.nditer(all_us, flags=['multi_index'], op_flags=['readwrite']) - while not it.finished: - # get index - idx = it.multi_index - # save and return - tmp_val = all_us[idx] - - # 差分を取る - # 上側の差分 - all_us[idx] = float(tmp_val) + h - us = all_us[:, :input_size] - x_1s, x_2s, lam_1s, lam_2s = forward_prop(us) # forward - fuh1 = grad_f(x_1s, x_2s, lam_1s, lam_2s, all_us) - - # 下側の差分 - all_us[idx] = float(tmp_val) - h - us = all_us[:, :input_size] - x_1s, x_2s, lam_1s, lam_2s = forward_prop(us) # forward - fuh2 = grad_f(x_1s, x_2s, lam_1s, lam_2s, all_us) - - grad[:, grad_idx] = ((fuh1 - fuh2) / (2.*h)).flatten() # to flat でgradに代入できるように - - all_us[idx] = tmp_val - it.iternext() - grad_idx += 1 - - return np.array(grad) - -class NMPCControllerWithNewton(): - """ - Attributes - ------------ - N : int - predicte step, discritize value - threshold : float - newton's threshold value - NUM_INPUT : int - system input length, this should include dummy u and constraint variables - MAX_ITERATION : int - decide by the solved matrix size - simulator : NMPCSimulatorSystem class - us : list of float - estimated optimal system input - dummy_us : list of float - estimated dummy input - raws : list of float - estimated constraint variable - history_u : list of float - time history of actual system input - history_dummy_u : list of float - time history of actual dummy u - history_raw : list of float - time history of actual raw - history_f : list of float - time history of error of optimal - """ - def __init__(self): - """ - Parameters - ----------- - None - """ - # parameters - self.N = 10 # time step - self.threshold = 0.0001 # break - - self.NUM_ALL_INPUT = 3 # u with dummy, and 制約条件に対するrawにも合わせた入力の数 - self.NUM_INPUT = 1 # u with dummy, and 制約条件に対するrawにも合わせた入力の数 - self.Jacobian_size = self.NUM_ALL_INPUT * self.N - - # newton parameters - self.MAX_ITERATION = 100 - - # simulator - self.simulator = NMPCSimulatorSystem() - - # initial - self.us = np.zeros((self.N, self.NUM_INPUT)) - self.dummy_us = np.ones((self.N, self.NUM_INPUT)) * 0.25 - self.raws = np.ones((self.N, self.NUM_INPUT)) * 0.01 - - # for fig - self.history_u = [] - self.history_dummy_u = [] - self.history_raw = [] - self.history_f = [] - - def calc_input(self, x_1, x_2, time): - """ - Parameters - ------------ - x_1 : float - current state - x_2 : float - current state - time : float in seconds - now time - Returns - -------- - us : list of float - estimated optimal system input - """ - # calculating sampling time - dt = 0.01 - - # concat all us, shape (pred_len, input_size) - all_us = np.hstack((self.us, self.dummy_us, self.raws)) - - # Newton method - for i in range(self.MAX_ITERATION): - # calc all state - x_1s, x_2s, lam_1s, lam_2s = self.simulator.calc_predict_and_adjoint_state(x_1, x_2, self.us, self.N, dt) - - # F - F_hat = self._calc_f(x_1s, x_2s, lam_1s, lam_2s, all_us, self.N, dt) - - # judge - if np.linalg.norm(F_hat) < self.threshold: - # print("break!!") - break - - grad_f = lambda x_1s, x_2s, lam_1s, lam_2s, all_us : self._calc_f(x_1s, x_2s, lam_1s, lam_2s, all_us, self.N, dt) - forward_prop_f = lambda us : self.simulator.calc_predict_and_adjoint_state(x_1, x_2, us, self.N, dt) - grads = calc_numerical_gradient(forward_prop_f, grad_f, all_us, (self.Jacobian_size, self.Jacobian_size), self.NUM_INPUT) - - # make jacobian and calc inverse of it - # grads += np.eye(self.Jacobian_size) * 1e-8 - try: - all_us = all_us.reshape(-1, 1) - np.dot(np.linalg.inv(grads), F_hat.reshape(-1, 1)) - except np.linalg.LinAlgError: - print("Warning : singular matrix!!") - grads += np.eye(self.Jacobian_size) * 1e-10 # add noise - all_us = all_us.reshape(-1, 1) - np.dot(np.linalg.inv(grads), F_hat.reshape(-1, 1)) - - all_us = all_us.reshape(self.N, self.NUM_ALL_INPUT) - - # update - self.us = all_us[:, :self.NUM_INPUT] - self.dummy_us = all_us[:, self.NUM_INPUT:2*self.NUM_INPUT] - self.raws = all_us[:, 2*self.NUM_INPUT:] - - # final insert - self.us = all_us[:, :self.NUM_INPUT] - self.dummy_us = all_us[:, self.NUM_INPUT:2*self.NUM_INPUT] - self.raws = all_us[:, 2*self.NUM_INPUT:] - - x_1s, x_2s, lam_1s, lam_2s = self.simulator.calc_predict_and_adjoint_state(x_1, x_2, self.us, self.N, dt) - - F = self._calc_f(x_1s, x_2s, lam_1s, lam_2s, all_us, self.N, dt) - - # for save - self.history_f.append(np.linalg.norm(F)) - self.history_u.append(self.us[0]) - self.history_dummy_u.append(self.dummy_us[0]) - self.history_raw.append(self.raws[0]) - - return self.us - - def _calc_f(self, x_1s, x_2s, lam_1s, lam_2s, all_us, N, dt): - """ - Parameters - ------------ - x_1s : list of float - predicted x_1s for N steps - x_2s : list of float - predicted x_2s for N steps - lam_1s : list of float - adjoint state of x_1s, lam_1s for N steps - lam_2s : list of float - adjoint state of x_2s, lam_2s for N steps - us : list of float - estimated optimal system input - dummy_us : list of float - estimated dummy input - raws : list of float - estimated constraint variable - N : int - predict time step - dt : float - sampling time of system - """ - F = np.zeros((N, self.NUM_INPUT*3)) - - us = all_us[:, :self.NUM_INPUT].flatten() - dummy_us = all_us[:, self.NUM_INPUT:2*self.NUM_INPUT].flatten() - raws = all_us[:, 2*self.NUM_INPUT:].flatten() - - for i in range(N): - F_u = 0.5 * us[i] + lam_2s[i] + 2. * raws[i] * us[i] - F_dummy = -0.01 + 2. * raws[i] * dummy_us[i] - F_raw = us[i]**2 + dummy_us[i]**2 - 0.5**2 - - F[i] = np.array([F_u, F_dummy, F_raw]) - - return np.array(F) - -def main(): - # simulation time - dt = 0.01 - iteration_time = 20. - iteration_num = int(iteration_time/dt) - - # plant - plant_system = SampleSystem(init_x_1=2., init_x_2=0.) - - # controller - controller = NMPCControllerWithNewton() - - # for i in range(iteration_num) - for i in range(1, iteration_num): - print("iteration = {}".format(i)) - time = float(i) * dt - x_1 = plant_system.x_1 - x_2 = plant_system.x_2 - # make input - us = controller.calc_input(x_1, x_2, time) - # update state - plant_system.update_state(us[0]) - - # figure - fig = plt.figure() - - x_1_fig = fig.add_subplot(321) - x_2_fig = fig.add_subplot(322) - u_fig = fig.add_subplot(323) - dummy_fig = fig.add_subplot(324) - raw_fig = fig.add_subplot(325) - f_fig = fig.add_subplot(326) - - x_1_fig.plot(np.arange(iteration_num)*dt, plant_system.history_x_1) - x_1_fig.set_xlabel("time [s]") - x_1_fig.set_ylabel("x_1") - - x_2_fig.plot(np.arange(iteration_num)*dt, plant_system.history_x_2) - x_2_fig.set_xlabel("time [s]") - x_2_fig.set_ylabel("x_2") - - u_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_u) - u_fig.set_xlabel("time [s]") - u_fig.set_ylabel("u") - - dummy_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_dummy_u) - dummy_fig.set_xlabel("time [s]") - dummy_fig.set_ylabel("dummy u") - - raw_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_raw) - raw_fig.set_xlabel("time [s]") - raw_fig.set_ylabel("raw") - - f_fig.plot(np.arange(iteration_num - 1)*dt, controller.history_f) - f_fig.set_xlabel("time [s]") - f_fig.set_ylabel("optimal error") - - fig.tight_layout() - - plt.show() - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/simple_run.py b/scripts/simple_run.py new file mode 100644 index 0000000..510e24e --- /dev/null +++ b/scripts/simple_run.py @@ -0,0 +1,53 @@ +import argparse + +from PythonLinearNonlinearControl.helper import bool_flag, make_logger +from PythonLinearNonlinearControl.controllers.make_controllers import make_controller +from PythonLinearNonlinearControl.planners.make_planners import make_planner +from PythonLinearNonlinearControl.configs.make_configs import make_config +from PythonLinearNonlinearControl.models.make_models import make_model +from PythonLinearNonlinearControl.envs.make_envs import make_env +from PythonLinearNonlinearControl.runners.make_runners import make_runner +from PythonLinearNonlinearControl.plotters.plot_func import plot_results + +def run(args): + # logger + make_logger(args.result_dir) + + # make envs + env = make_env(args) + + # make config + config = make_config(args) + + # make planner + planner = make_planner(args, config) + + # make model + model = make_model(args, config) + + # make controller + controller = make_controller(args, config, model) + + # make simulator + runner = make_runner(args) + + # run experiment + history_x, history_u, history_g = runner.run(env, controller, planner) + + # plot results + plot_results(args, history_x, history_u, history_g=history_g) + +def main(): + parser = argparse.ArgumentParser() + + parser.add_argument("--controller_type", type=str, default="MPC") + parser.add_argument("--planner_type", type=str, default="const") + parser.add_argument("--env", type=str, default="FirstOrderLag") + parser.add_argument("--result_dir", type=str, default="./result") + + args = parser.parse_args() + + run(args) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7ea6a7c --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +from setuptools import find_packages +from setuptools import setup + +install_requires = ['numpy', 'matplotlib', 'cvxopt'] +tests_require = ['pytest'] +setup_requires = ["pytest-runner"] + +setup( + name='PythonLinearNonlinearControl', + version='2.0', + description='Implementing linear and non-linear control method in python', + author='Shunichi Sekiguchi', + author_email='quick1st97of@gmail.com', + install_requires=install_requires, + url='https://github.com/Shunichi09/PythonLinearNonlinearControl', + license='MIT License', + packages=find_packages(exclude=('tests')), + setup_requires=setup_requires, + test_suite='tests', + tests_require=tests_require +) \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/configs/__init__.py b/tests/configs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/configs/test_first_order_lag.py b/tests/configs/test_first_order_lag.py new file mode 100644 index 0000000..a013fac --- /dev/null +++ b/tests/configs/test_first_order_lag.py @@ -0,0 +1,34 @@ +import pytest +import numpy as np + +from PythonLinearNonlinearControl.configs.first_order_lag \ + import FirstOrderLagConfigModule + +class TestCalcCost(): + def test_calc_costs(self): + # make config + config = FirstOrderLagConfigModule() + # set + pred_len = 5 + state_size = 4 + input_size = 2 + pop_size = 2 + pred_xs = np.ones((pop_size, pred_len, state_size)) + g_xs = np.ones((pop_size, pred_len, state_size)) * 0.5 + input_samples = np.ones((pop_size, pred_len, input_size)) * 0.5 + + costs = config.input_cost_fn(input_samples) + expected_costs = np.ones((pop_size, pred_len, input_size))*0.5 + + assert costs == pytest.approx(expected_costs**2 * np.diag(config.R)) + + costs = config.state_cost_fn(pred_xs, g_xs) + expected_costs = np.ones((pop_size, pred_len, state_size))*0.5 + + assert costs == pytest.approx(expected_costs**2 * np.diag(config.Q)) + + costs = config.terminal_state_cost_fn(pred_xs[:, -1, :],\ + g_xs[:, -1, :]) + expected_costs = np.ones((pop_size, state_size))*0.5 + + assert costs == pytest.approx(expected_costs**2 * np.diag(config.Sf)) \ No newline at end of file diff --git a/tests/models/__init__.py b/tests/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/models/test_model.py b/tests/models/test_model.py new file mode 100644 index 0000000..e11a439 --- /dev/null +++ b/tests/models/test_model.py @@ -0,0 +1,53 @@ +import pytest +import numpy as np + +from PythonLinearNonlinearControl.models.model import LinearModel + +class TestLinearModel(): + """ + """ + def test_predict(self): + + A = np.array([[1., 0.1], + [0.1, 1.5]]) + B = np.array([[0.2], [0.5]]) + curr_x = np.ones(2) * 0.5 + u = np.ones((1, 1)) + + linear_model = LinearModel(A, B) + pred_xs = linear_model.predict_traj(curr_x, u) + + assert pred_xs == pytest.approx(np.array([[0.5, 0.5], [0.75, 1.3]])) + + def test_alltogether(self): + + A = np.array([[1., 0.1], + [0.1, 1.5]]) + B = np.array([[0.2], [0.5]]) + curr_x = np.ones(2) * 0.5 + u = np.ones((1, 1)) + + linear_model = LinearModel(A, B) + pred_xs = linear_model.predict_traj(curr_x, u) + + u = np.tile(u, (1, 1, 1)) + pred_xs_alltogether = linear_model.predict_traj(curr_x, u)[0] + + assert pred_xs_alltogether == pytest.approx(pred_xs) + + def test_alltogether_val(self): + + A = np.array([[1., 0.1], + [0.1, 1.5]]) + B = np.array([[0.2], [0.5]]) + curr_x = np.ones(2) * 0.5 + u = np.stack((np.ones((1, 1)), np.ones((1, 1))*0.5), axis=0) + + linear_model = LinearModel(A, B) + + pred_xs_alltogether = linear_model.predict_traj(curr_x, u) + + expected_val = np.array([[[0.5, 0.5], [0.75, 1.3]], + [[0.5, 0.5], [0.65, 1.05]]]) + + assert pred_xs_alltogether == pytest.approx(expected_val) \ No newline at end of file From ac7ab11fa0da9e3b1973e592f61a955c54f3358b Mon Sep 17 00:00:00 2001 From: Shunichi09 Date: Thu, 2 Apr 2020 17:37:09 +0900 Subject: [PATCH 2/3] Add: two wheeled model and environment --- .travis.yml | 14 ++ .../configs/first_order_lag.py | 2 - .../configs/make_configs.py | 5 +- .../configs/two_wheeled.py | 86 ++++++++++++ .../envs/first_order_lag.py | 12 +- .../envs/make_envs.py | 5 +- .../envs/two_wheeled.py | 125 ++++++------------ .../models/make_models.py | 3 + .../models/two_wheeled.py | 53 ++++++++ README.md | 31 +++-- assets/concept.png | Bin 0 -> 216364 bytes scripts/simple_run.py | 4 +- setup.py | 4 +- tests/models/test_two_wheeled.py | 42 ++++++ 14 files changed, 277 insertions(+), 109 deletions(-) create mode 100644 .travis.yml create mode 100644 PythonLinearNonlinearControl/configs/two_wheeled.py create mode 100644 PythonLinearNonlinearControl/models/two_wheeled.py create mode 100644 assets/concept.png create mode 100644 tests/models/test_two_wheeled.py diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..99e1756 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +language: python + +python: + - 3.7 + +install: + - pip install --upgrade pip setuptools wheel + - pip install coveralls + +script: + - coverage run --source=PythonLinearNonlinearControl setup.py test + +after_success: + - coveralls \ No newline at end of file diff --git a/PythonLinearNonlinearControl/configs/first_order_lag.py b/PythonLinearNonlinearControl/configs/first_order_lag.py index 18f86c9..166b3d0 100644 --- a/PythonLinearNonlinearControl/configs/first_order_lag.py +++ b/PythonLinearNonlinearControl/configs/first_order_lag.py @@ -23,8 +23,6 @@ class FirstOrderLagConfigModule(): def __init__(self): """ - Args: - save_dit (str): save directory """ # opt configs self.opt_config = { diff --git a/PythonLinearNonlinearControl/configs/make_configs.py b/PythonLinearNonlinearControl/configs/make_configs.py index fb54981..87e3709 100644 --- a/PythonLinearNonlinearControl/configs/make_configs.py +++ b/PythonLinearNonlinearControl/configs/make_configs.py @@ -1,4 +1,5 @@ from .first_order_lag import FirstOrderLagConfigModule +from .two_wheeled import TwoWheeledConfigModule def make_config(args): """ @@ -6,4 +7,6 @@ def make_config(args): config (ConfigModule class): configuration for the each env """ if args.env == "FirstOrderLag": - return FirstOrderLagConfigModule() \ No newline at end of file + return FirstOrderLagConfigModule() + elif args.env == "TwoWheeledConst" or args.env == "TwoWheeled": + return TwoWheeledConfigModule() \ No newline at end of file diff --git a/PythonLinearNonlinearControl/configs/two_wheeled.py b/PythonLinearNonlinearControl/configs/two_wheeled.py new file mode 100644 index 0000000..f8f2692 --- /dev/null +++ b/PythonLinearNonlinearControl/configs/two_wheeled.py @@ -0,0 +1,86 @@ +import numpy as np + +class TwoWheeledConfigModule(): + # parameters + ENV_NAME = "TwoWheeled-v0" + TYPE = "Nonlinear" + TASK_HORIZON = 1000 + PRED_LEN = 10 + STATE_SIZE = 3 + INPUT_SIZE = 2 + DT = 0.01 + # cost parameters + R = np.eye(INPUT_SIZE) + Q = np.eye(STATE_SIZE) + Sf = np.eye(STATE_SIZE) + # bounds + INPUT_LOWER_BOUND = np.array([-1.5, 3.14]) + INPUT_UPPER_BOUND = np.array([1.5, 3.14]) + + def __init__(self): + """ + """ + # opt configs + self.opt_config = { + "Random": { + "popsize": 5000 + }, + "CEM": { + "popsize": 500, + "num_elites": 50, + "max_iters": 15, + "alpha": 0.3, + "init_var":1., + "threshold":0.001 + }, + "MPPI":{ + "beta" : 0.6, + "popsize": 5000, + "kappa": 0.9, + "noise_sigma": 0.5, + }, + "iLQR":{ + }, + "NMPC-CGMRES":{ + }, + "NMPC-Newton":{ + }, + } + + @staticmethod + def input_cost_fn(u): + """ input cost functions + Args: + u (numpy.ndarray): input, shape(input_size, ) + or shape(pop_size, input_size) + Returns: + cost (numpy.ndarray): cost of input, none or shape(pop_size, ) + """ + return (u**2) * np.diag(TwoWheeledConfigModule.R) * 0.1 + + @staticmethod + def state_cost_fn(x, g_x): + """ state cost function + Args: + x (numpy.ndarray): state, shape(pred_len, state_size) + or shape(pop_size, pred_len, state_size) + g_x (numpy.ndarray): goal state, shape(state_size, ) + or shape(pop_size, state_size) + Returns: + cost (numpy.ndarray): cost of state, none or shape(pop_size, ) + """ + return ((x - g_x)**2) * np.diag(TwoWheeledConfigModule.Q) + + @staticmethod + def terminal_state_cost_fn(terminal_x, terminal_g_x): + """ + Args: + terminal_x (numpy.ndarray): terminal state, + shape(state_size, ) or shape(pop_size, state_size) + terminal_g_x (numpy.ndarray): terminal goal state, + shape(state_size, ) or shape(pop_size, state_size) + Returns: + cost (numpy.ndarray): cost of state, none or shape(pop_size, ) + """ + return ((terminal_x - terminal_g_x)**2) \ + * np.diag(TwoWheeledConfigModule.Sf) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/first_order_lag.py b/PythonLinearNonlinearControl/envs/first_order_lag.py index 0348ee0..8d0588d 100644 --- a/PythonLinearNonlinearControl/envs/first_order_lag.py +++ b/PythonLinearNonlinearControl/envs/first_order_lag.py @@ -70,13 +70,13 @@ class FirstOrderLagEnv(Env): self.curr_x = init_x # goal - self.goal_state = np.array([0., 0, -2., 3.]) + self.g_x = np.array([0., 0, -2., 3.]) # clear memory self.history_x = [] self.history_g_x = [] - return self.curr_x, {"goal_state": self.goal_state} + return self.curr_x, {"goal_state": self.g_x} def step(self, u): """ @@ -99,15 +99,17 @@ class FirstOrderLagEnv(Env): # cost cost = 0 cost = np.sum(u**2) - cost += np.sum((self.curr_x-g_x)**2) + cost += np.sum((self.curr_x - self.g_x)**2) # save history self.history_x.append(next_x.flatten()) - self.history_g_x.append(self.goal_state.flatten()) + self.history_g_x.append(self.g_x.flatten()) # update self.curr_x = next_x.flatten() # update costs self.step_count += 1 - return next_x.flatten(), cost, self.step_count > self.config["max_step"], {"goal_state" : self.goal_state} \ No newline at end of file + return next_x.flatten(), cost, \ + self.step_count > self.config["max_step"], \ + {"goal_state" : self.g_x} \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/make_envs.py b/PythonLinearNonlinearControl/envs/make_envs.py index aedd299..debbf29 100644 --- a/PythonLinearNonlinearControl/envs/make_envs.py +++ b/PythonLinearNonlinearControl/envs/make_envs.py @@ -1,8 +1,11 @@ from .first_order_lag import FirstOrderLagEnv +from .two_wheeled import TwoWheeledConstEnv def make_env(args): if args.env == "FirstOrderLag": return FirstOrderLagEnv() + elif args.env == "TwoWheeledConst": + return TwoWheeledConstEnv() - raise NotImplementedError("There is not {} Env".format(name)) \ No newline at end of file + raise NotImplementedError("There is not {} Env".format(args.env)) \ No newline at end of file diff --git a/PythonLinearNonlinearControl/envs/two_wheeled.py b/PythonLinearNonlinearControl/envs/two_wheeled.py index 55f08f6..0d99874 100644 --- a/PythonLinearNonlinearControl/envs/two_wheeled.py +++ b/PythonLinearNonlinearControl/envs/two_wheeled.py @@ -1,8 +1,30 @@ import numpy as np -import scipy -from scipy import integrate + from .env import Env +def step_two_wheeled_env(curr_x, u, dt, method="Oylar"): + """ step two wheeled enviroment + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) + u (numpy.ndarray): input, shape(input_size, ) + dt (float): sampling time + Returns: + next_x (numpy.ndarray): next state, shape(state_size. ) + + Notes: + TODO: deal with another method, like Runge Kutta + """ + B = np.array([[np.cos(curr_x[-1]), 0.], + [np.sin(curr_x[-1]), 0.], + [0., 1.]]) + + x_dot = np.matmul(B, u[:, np.newaxis]) + + next_x = x_dot.flatten() * dt + curr_x + + return next_x + class TwoWheeledConstEnv(Env): """ Two wheeled robot with constant goal Env """ @@ -12,15 +34,16 @@ class TwoWheeledConstEnv(Env): self.config = {"state_size" : 3,\ "input_size" : 2,\ "dt" : 0.01,\ - "max_step" : 500,\ + "max_step" : 1000,\ "input_lower_bound": [-1.5, -3.14],\ "input_upper_bound": [1.5, 3.14], } - super(TwoWheeledEnv, self).__init__(self.config) + super(TwoWheeledConstEnv, self).__init__(self.config) def reset(self, init_x=None): """ reset state + Returns: init_x (numpy.ndarray): initial state, shape(state_size, ) info (dict): information @@ -33,16 +56,17 @@ class TwoWheeledConstEnv(Env): self.curr_x = init_x # goal - self.goal_state = np.array([0., 0, -2., 3.]) + self.g_x = np.array([5., 5., 0.]) # clear memory self.history_x = [] self.history_g_x = [] - return self.curr_x, {"goal_state": self.goal_state} + return self.curr_x, {"goal_state": self.g_x} def step(self, u): - """ + """ step environments + Args: u (numpy.ndarray) : input, shape(input_size, ) Returns: @@ -54,92 +78,25 @@ class TwoWheeledConstEnv(Env): # clip action u = np.clip(u, self.config["input_lower_bound"], - self.config["input_lower_bound"]) + self.config["input_upper_bound"]) # step - next_x = np.matmul(self.A, self.curr_x[:, np.newaxis]) \ - + np.matmul(self.B, u[:, np.newaxis]) + next_x = step_two_wheeled_env(self.curr_x, u, self.config["dt"]) - # TODO: implement costs + # TODO: costs + costs = 0. + costs += 0.1 * np.sum(u**2) + costs += np.sum((self.curr_x - self.g_x)**2) # save history self.history_x.append(next_x.flatten()) - self.history_g_x.append(self.goal_state.flatten()) + self.history_g_x.append(self.g_x.flatten()) # update self.curr_x = next_x.flatten() # update costs self.step_count += 1 - return next_x.flatten(), 0., self.step_count > self.config["max_step"], {"goal_state" : self.goal_state} - -class TwoWheeledEnv(Env): - """ Two wheeled robot Env - """ - def __init__(self): - """ - """ - self.config = {"state_size" : 3,\ - "input_size" : 2,\ - "dt" : 0.01,\ - "max_step" : 500,\ - "input_lower_bound": [-1.5, -3.14],\ - "input_upper_bound": [1.5, 3.14], - } - - super(TwoWheeledEnv, self).__init__(self.config) - - def reset(self, init_x=None): - """ reset state - Returns: - init_x (numpy.ndarray): initial state, shape(state_size, ) - info (dict): information - """ - self.step_count = 0 - - self.curr_x = np.zeros(self.config["state_size"]) - - if init_x is not None: - self.curr_x = init_x - - # goal - self.goal_state = np.array([0., 0, -2., 3.]) - - # clear memory - self.history_x = [] - self.history_g_x = [] - - return self.curr_x, {"goal_state": self.goal_state} - - def step(self, u): - """ - Args: - u (numpy.ndarray) : input, shape(input_size, ) - Returns: - next_x (numpy.ndarray): next state, shape(state_size, ) - cost (float): costs - done (bool): end the simulation or not - info (dict): information - """ - # clip action - u = np.clip(u, - self.config["input_lower_bound"], - self.config["input_lower_bound"]) - - # step - next_x = np.matmul(self.A, self.curr_x[:, np.newaxis]) \ - + np.matmul(self.B, u[:, np.newaxis]) - - # TODO: implement costs - - # save history - self.history_x.append(next_x.flatten()) - self.history_g_x.append(self.goal_state.flatten()) - - # update - self.curr_x = next_x.flatten() - # update costs - self.step_count += 1 - - return next_x.flatten(), 0., self.step_count > self.config["max_step"], {"goal_state" : self.goal_state} - + return next_x.flatten(), costs, \ + self.step_count > self.config["max_step"], \ + {"goal_state" : self.g_x} \ No newline at end of file diff --git a/PythonLinearNonlinearControl/models/make_models.py b/PythonLinearNonlinearControl/models/make_models.py index 73c3987..7688f93 100644 --- a/PythonLinearNonlinearControl/models/make_models.py +++ b/PythonLinearNonlinearControl/models/make_models.py @@ -1,8 +1,11 @@ from .first_order_lag import FirstOrderLagModel +from .two_wheeled import TwoWheeledModel def make_model(args, config): if args.env == "FirstOrderLag": return FirstOrderLagModel(config) + elif args.env == "TwoWheeledConst" or args.env == "TwoWheeled": + return TwoWheeledModel(config) raise NotImplementedError("There is not {} Model".format(args.env)) diff --git a/PythonLinearNonlinearControl/models/two_wheeled.py b/PythonLinearNonlinearControl/models/two_wheeled.py new file mode 100644 index 0000000..babb86f --- /dev/null +++ b/PythonLinearNonlinearControl/models/two_wheeled.py @@ -0,0 +1,53 @@ +import numpy as np + +from .model import Model + +class TwoWheeledModel(Model): + """ two wheeled model + """ + def __init__(self, config): + """ + """ + super(TwoWheeledModel, self).__init__() + self.dt = config.DT + + def predict_next_state(self, curr_x, u): + """ predict next state + + Args: + curr_x (numpy.ndarray): current state, shape(state_size, ) or + shape(pop_size, state_size) + u (numpy.ndarray): input, shape(input_size, ) or + shape(pop_size, input_size) + Returns: + next_x (numpy.ndarray): next state, shape(state_size, ) or + shape(pop_size, state_size) + """ + if len(u.shape) == 1: + B = np.array([[np.cos(curr_x[-1]), 0.], + [np.sin(curr_x[-1]), 0.], + [0., 1.]]) + # calc dot + x_dot = np.matmul(B, u[:, np.newaxis]) + # next state + next_x = x_dot.flatten() * self.dt + curr_x + + return next_x + + elif len(u.shape) == 2: + (pop_size, state_size) = curr_x.shape + (_, input_size) = u.shape + # B.shape = (pop_size, state_size, input_size) + B = np.zeros((pop_size, state_size, input_size)) + # insert + B[:, 0, 0] = np.cos(curr_x[:, -1]) + B[:, 1, 0] = np.sin(curr_x[:, -1]) + B[:, 2, 1] = np.ones(pop_size) + + # x_dot.shape = (pop_size, state_size, 1) + x_dot = np.matmul(B, u[:, :, np.newaxis]) + # next state + next_x = x_dot[:, :, 0] * self.dt + curr_x + + return next_x + \ No newline at end of file diff --git a/README.md b/README.md index e4381d4..5e6c523 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,25 @@ [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +[![Coverage Status](https://coveralls.io/repos/github/Shunichi09/PythonLinearNonlinearControl/badge.svg?branch=master)](https://coveralls.io/github/Shunichi09/PythonLinearNonlinearControl?branch=master) +[![Build Status](https://travis-ci.org/Shunichi09/PythonLinearNonlinearControl.svg?branch=master)](https://travis-ci.org/Shunichi09/PythonLinearNonlinearControl) # PythonLinearNonLinearControl PythonLinearNonLinearControl is a library implementing the linear and nonlinear control theories in python. +![Concepts](assets/concepts.png) + # Algorithms | Algorithm | Use Linear Model | Use Nonlinear Model | Need Gradient (Hamiltonian) | Need Gradient (Model) | -|:----------|:---------------:|:----------------:|:----------------:| +|:----------|:---------------: |:----------------:|:----------------:|:----------------:| | Linear Model Predictive Control (MPC) | ✓ | x | x | x | | Cross Entropy Method (CEM) | ✓ | ✓ | x | x | | Model Preidictive Path Integral Control (MPPI) | ✓ | ✓ | x | x | | Random Shooting Method (Random) | ✓ | ✓ | x | x | -| Iterative LQR (iLQR) | ✓ | ✓ | x | ✓ | -| Nonlinear Model Predictive Control -CGMRES- (NMPC-CGMRES) | ✓ | ✓ | ✓ | x | -| Nonlinear Model Predictive Control -Newton- (NMPC-Newton) | ✓ | ✓ | x | x | +| Iterative LQR (iLQR) | x | ✓ | x | ✓ | +| Unconstrained Nonlinear Model Predictive Control (NMPC) | x | ✓ | ✓ | x | +| Constrained Nonlinear Model Predictive Control CGMRES (NMPC-CGMRES) | x | ✓ | ✓ | x | +| Constrained Nonlinear Model Predictive Control Newton (NMPC-Newton) | x | ✓ | x | x | "Need Gradient" means that you have to implement the gradient of the model or the gradient of hamiltonian. This library is also easily to extend for your own situations. @@ -35,24 +40,24 @@ Following algorithms are implemented in PythonLinearNonlinearControl - [script]() - [Iterative LQR (iLQR)](https://ieeexplore.ieee.org/document/6386025) - Ref: Tassa, Y., Erez, T., & Todorov, E. (2012, October). Synthesis and stabilization of complex behaviors through online trajectory optimization. In 2012 IEEE/RSJ International Conference on Intelligent Robots and Systems (pp. 4906-4913). IEEE. and [Study Wolf](https://github.com/studywolf/control) - - [script]() -- [Unconstrained Nonlinear Model Predictive Control](https://www.sciencedirect.com/science/article/pii/S0005109897000058) + - [script (Coming soon)]() +- [Unconstrained Nonlinear Model Predictive Control (NMPC)](https://www.sciencedirect.com/science/article/pii/S0005109897000058) - Ref: Ohtsuka, T., & Fujii, H. A. (1997). Real-time optimization algorithm for nonlinear receding-horizon control. Automatica, 33(6), 1147-1154. - - [script]() + - [script (Coming soon)]() - [Constrained Nonlinear Model Predictive Control -CGMRES- (NMPC-CGMRES)](https://www.sciencedirect.com/science/article/pii/S0005109897000058) - Ref: Ohtsuka, T., & Fujii, H. A. (1997). Real-time optimization algorithm for nonlinear receding-horizon control. Automatica, 33(6), 1147-1154. - - [script]() + - [script (Coming soon)]() - [Constrained Nonlinear Model Predictive Control -Newton- (NMPC-Newton)](https://www.sciencedirect.com/science/article/pii/S0005109897000058) - Ref: Ohtsuka, T., & Fujii, H. A. (1997). Real-time optimization algorithm for nonlinear receding-horizon control. Automatica, 33(6), 1147-1154. - - [script]() + - [script (Coming soon)]() # Environments | Name | Linear | Nonlinear | State Size | Input size | -|:----------|:---------------:|:----------------:|:----------------:| +|:----------|:---------------:|:----------------:|:----------------:|:----------------:| | First Order Lag System | ✓ | x | 4 | 2 | -| Auto Cruse Control System | x | ✓ | 4 | 2 | -| Two wheeled System | x | ✓ | 4 | 2 | +| Two wheeled System (Constant Goal) | x | ✓ | 3 | 2 | +| Two wheeled System (Moving Goal) (Coming soon) | x | ✓ | 3 | 2 | All environments are continuous. **It should be noted that the algorithms for linear model could be applied to nonlinear enviroments if you have linealized the model of nonlinear environments.** @@ -98,6 +103,8 @@ python scripts/simple_run.py --model "first-order_lag" --controller "CEM" When we design control systems, we should have **Model**, **Planner**, **Controller** and **Runner** as shown in the figure. It should be noted that **Model** and **Environment** are different. As mentioned before, we the algorithms for linear model could be applied to nonlinear enviroments if you have linealized model of nonlinear environments. In addition, you can use Neural Network or any non-linear functions to the model, although this library can not deal with it now. +![Concepts](assets/concepts.png) + ## Model System model. For an instance, in the case that a model is linear, this model should have a form, "x[k+1] = Ax[k] + Bu[k]". diff --git a/assets/concept.png b/assets/concept.png new file mode 100644 index 0000000000000000000000000000000000000000..042120263b9bab26382bc51941011b38121fedc5 GIT binary patch literal 216364 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

!GYv&PMh~E`j%kAzov9^ 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

iB!d?!5PkCYr6A29vaI@iCy)m3>8sL3t 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

h683Po}Sgb}9-WKTt%I!Oa&uj-Sk0dRB|LycI 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 Date: Thu, 2 Apr 2020 17:46:53 +0900 Subject: [PATCH 3/3] Update: added imgs to readme --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 5e6c523..e3c0767 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ PythonLinearNonLinearControl is a library implementing the linear and nonlinear control theories in python. -![Concepts](assets/concepts.png) + # Algorithms @@ -28,16 +28,16 @@ Following algorithms are implemented in PythonLinearNonlinearControl - [Linear Model Predictive Control (MPC)](http://www2.eng.cam.ac.uk/~jmm1/mpcbook/mpcbook.html) - Ref: Maciejowski, J. M. (2002). Predictive control: with constraints. - - [script]() + - [script](PythonLinearNonlinearControl/controllers/mpc.py) - [Cross Entropy Method (CEM)](https://arxiv.org/abs/1805.12114) - Ref: Chua, K., Calandra, R., McAllister, R., & Levine, S. (2018). Deep reinforcement learning in a handful of trials using probabilistic dynamics models. In Advances in Neural Information Processing Systems (pp. 4754-4765) - - [script]() + - [script](PythonLinearNonlinearControl/controllers/cem.py) - [Model Preidictive Path Integral Control (MPPI)](https://arxiv.org/abs/1909.11652) - Ref: Nagabandi, A., Konoglie, K., Levine, S., & Kumar, V. (2019). Deep Dynamics Models for Learning Dexterous Manipulation. arXiv preprint arXiv:1909.11652. - - [script]() + - [script](PythonLinearNonlinearControl/controllers/mppi.py) - [Random Shooting Method (Random)](https://arxiv.org/abs/1805.12114) - Ref: Chua, K., Calandra, R., McAllister, R., & Levine, S. (2018). Deep reinforcement learning in a handful of trials using probabilistic dynamics models. In Advances in Neural Information Processing Systems (pp. 4754-4765) - - [script]() + - [script](PythonLinearNonlinearControl/controllers/random.py) - [Iterative LQR (iLQR)](https://ieeexplore.ieee.org/document/6386025) - Ref: Tassa, Y., Erez, T., & Todorov, E. (2012, October). Synthesis and stabilization of complex behaviors through online trajectory optimization. In 2012 IEEE/RSJ International Conference on Intelligent Robots and Systems (pp. 4906-4913). IEEE. and [Study Wolf](https://github.com/studywolf/control) - [script (Coming soon)]() @@ -93,7 +93,7 @@ pip install -e . You can run the experiments as follows: ``` -python scripts/simple_run.py --model "first-order_lag" --controller "CEM" +python scripts/simple_run.py --model first-order_lag --controller CEM ``` **figures and animations are saved in the ./result folder.** @@ -103,7 +103,7 @@ python scripts/simple_run.py --model "first-order_lag" --controller "CEM" When we design control systems, we should have **Model**, **Planner**, **Controller** and **Runner** as shown in the figure. It should be noted that **Model** and **Environment** are different. As mentioned before, we the algorithms for linear model could be applied to nonlinear enviroments if you have linealized model of nonlinear environments. In addition, you can use Neural Network or any non-linear functions to the model, although this library can not deal with it now. -![Concepts](assets/concepts.png) + ## Model