ldsCtrlEst_h/lds_ctrl.h #
Controller. More…
Namespaces #
Name |
---|
lds Linear Dynamical Systems (LDS) namespace. |
Classes #
Name | |
---|---|
class | lds::Controller |
Detailed Description #
This file declares the type for control of a linear dynamical system (lds::Controller).
Source code #
//===-- ldsCtrlEst_h/lds_control.h - Controller -----------------*- C++ -*-===//
//
// Copyright 2021 Michael Bolus
// Copyright 2021 Georgia Institute of Technology
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
//===----------------------------------------------------------------------===//
//===----------------------------------------------------------------------===//
#ifndef LDSCTRLEST_LDS_CTRL_H
#define LDSCTRLEST_LDS_CTRL_H
// namespace
#include "lds.h"
// system type
#include "lds_sys.h"
namespace lds {
template <typename System>
class Controller {
static_assert(std::is_base_of<lds::System, System>::value,
"System must be derived from lds::System type.");
public:
Controller() = default;
Controller(const System& sys, data_t u_lb, data_t u_ub,
size_t control_type = 0);
Controller(System&& sys, data_t u_lb, data_t u_ub, size_t control_type = 0);
const Vector& Control(const Vector& z, bool do_control = true,
bool do_lock_control = false,
data_t sigma_soft_start = 0, data_t sigma_u_noise = 0,
bool do_reset_at_control_onset = true);
const Vector& ControlOutputReference(const Vector& z, bool do_control = true,
bool do_estimation = true,
bool do_lock_control = false,
data_t sigma_soft_start = 0,
data_t sigma_u_noise = 0,
bool do_reset_at_control_onset = true);
// get methods:
const System& sys() const { return sys_; };
const Matrix& Kc() const { return Kc_; };
const Matrix& Kc_inty() const { return Kc_inty_; };
const Matrix& Kc_u() const { return Kc_u_; };
const Vector& g_design() const { return g_design_; };
const Vector& u_ref() const { return u_ref_; };
const Vector& x_ref() const { return x_ref_; };
const Vector& y_ref() const { return y_ref_; };
size_t control_type() const { return control_type_; };
data_t tau_awu() const { return tau_awu_; };
data_t u_lb() const { return u_lb_; };
data_t u_ub() const { return u_ub_; };
// set methods
void set_sys(const System& sys) {
bool does_match = sys_.n_u() == sys.n_u();
does_match = does_match && (sys_.n_x() == sys.n_x());
does_match = does_match && (sys_.n_y() == sys.n_y());
if (does_match) {
sys_ = sys;
} else {
throw std::runtime_error(
"new system argument to `set_sys` does not match dimensionality of "
"existing system");
}
};
void set_g_design(const Vector& g_design) { Reassign(g_design_, g_design); };
void set_u_ref(const Vector& u_ref) { Reassign(u_ref_, u_ref); };
void set_x_ref(const Vector& x_ref) {
Reassign(x_ref_, x_ref);
cx_ref_ = sys_.C() * x_ref_;
};
// y_ref needs to be handled differently depending on output fn.
// (need to populate cx_ref_ too, which depends on output fn)
virtual void set_y_ref(const Vector& y_ref) { Reassign(y_ref_, y_ref); };
void set_Kc(const Matrix& Kc) { Reassign(Kc_, Kc); };
void set_Kc_inty(const Matrix& Kc_inty) { Reassign(Kc_inty_, Kc_inty); };
void set_Kc_u(const Matrix& Kc_u) { Reassign(Kc_u_, Kc_u); };
void set_tau_awu(data_t tau) {
tau_awu_ = tau;
k_awu_ = sys_.dt() / tau_awu_;
};
void set_control_type(size_t control_type);
// There is no reason u_lb/ub should not be public, but making set methods
// anyway.
void set_u_lb(data_t u_lb) { u_lb_ = u_lb; };
void set_u_ub(data_t u_ub) { u_ub_ = u_ub; };
void Reset() {
sys_.Reset();
u_ref_.zeros();
u_ref_prev_.zeros();
int_e_.zeros();
int_e_awu_adjust_.zeros();
u_sat_.zeros();
u_saturated_ = false;
t_since_control_onset_ = 0.0;
};
void Print() {
sys_.Print();
std::cout << "g_design : " << g_design_ << "\n";
std::cout << "u_lb : " << u_lb_ << "\n";
std::cout << "u_ub : " << u_ub_ << "\n";
};
protected:
System sys_;
Vector u_;
Vector u_return_;
Vector g_design_;
// reference signals
Vector u_ref_;
// create no set method for this:
Vector u_ref_prev_;
Vector x_ref_;
Vector y_ref_;
Vector cx_ref_;
// Controller gains
Matrix Kc_;
Matrix
Kc_u_;
Matrix Kc_inty_;
// control after g inversion
// do not need set methods for these.
Vector du_ref_;
Vector dv_ref_;
Vector v_ref_;
Vector dv_;
Vector v_;
// integral error
// do not need set method for this
Vector int_e_;
Vector int_e_awu_adjust_;
Vector u_sat_;
bool do_control_prev_ = false;
bool do_lock_control_prev_ = false;
// whether the g of system has become inverted from what you think it is
// (gain_ref)
bool u_saturated_ =
false;
// should be safe to have references here bc nothing needs to be done
// (like reset vars) when it changes...
data_t u_lb_{};
data_t u_ub_{};
data_t tau_awu_{};
data_t k_awu_ = 0;
data_t t_since_control_onset_ = 0;
size_t control_type_{};
private:
void CalcControl(bool do_control = true, bool do_estimation = true,
bool do_lock_control = false, data_t sigma_soft_start = 0,
data_t sigma_u_noise = 0,
bool do_reset_at_control_onset = true);
void CalcSteadyStateSetPoint();
void AntiWindup();
void InitVars(size_t control_type);
};
// Implement the above:
template <typename System>
inline Controller<System>::Controller(const System& sys, data_t u_lb,
data_t u_ub, size_t control_type)
: sys_(sys),
u_lb_(u_lb),
u_ub_(u_ub),
tau_awu_(lds::kInf) {
InitVars(control_type);
}
template <typename System>
inline Controller<System>::Controller(System&& sys, data_t u_lb, data_t u_ub,
size_t control_type)
: sys_(std::move(sys)),
u_lb_(u_lb),
u_ub_(u_ub),
tau_awu_(lds::kInf) {
InitVars(control_type);
}
template <typename System>
inline void Controller<System>::set_control_type(size_t control_type) {
if (control_type_ == control_type) {
return;
}
// creating a blank slate...
control_type_ = 0;
Kc_inty_.zeros(0, 0);
Kc_u_.zeros(0, 0);
int_e_.zeros(0, 0);
int_e_awu_adjust_.zeros(0, 0);
// controller was designed to minimize integral error
if (control_type & kControlTypeIntY) {
Kc_inty_.zeros(sys_.n_u(), sys_.n_y());
int_e_.zeros(sys_.n_y());
int_e_awu_adjust_.zeros(sys_.n_u());
control_type_ = control_type_ | kControlTypeIntY;
}
// controller was designed to minimize deltaU
// (i.e. state augmented with u)
if (control_type & kControlTypeDeltaU) {
Kc_u_.zeros(sys_.n_u(), sys_.n_u());
control_type_ = control_type_ | kControlTypeDeltaU;
}
// whether to adapt set point calculate with (re-estimated) process
// disturbance (m)
if (control_type & kControlTypeAdaptM) {
if (sys_.do_adapt_m) // only if adapting m...
{
control_type_ = control_type_ | kControlTypeAdaptM;
}
}
} // set_control_type
template <typename System>
inline const Vector& Controller<System>::Control(
const Vector& z, bool do_control, bool do_lock_control,
data_t sigma_soft_start, data_t sigma_u_noise,
bool do_reset_at_control_onset) {
// update state estimates, given latest measurement
sys_.Filter(u_, z);
bool do_estimation = true; // always have estimator on in this case
// calculate control signal
CalcControl(do_control, do_estimation, do_lock_control, sigma_soft_start,
sigma_u_noise, do_reset_at_control_onset);
return u_return_;
}
template <typename System>
inline const Vector& Controller<System>::ControlOutputReference(
const Vector& z, bool do_control, bool do_estimation, bool do_lock_control,
data_t sigma_soft_start, data_t sigma_u_noise,
bool do_reset_at_control_onset) {
// update state estimates, given latest measurement
if (do_estimation) {
sys_.Filter(u_, z);
} else {
sys_.f(u_);
}
// calculate the set point
// solves for u_ref and x_ref when output is at y_ref at steady state.
if (do_control) {
CalcSteadyStateSetPoint();
}
// calculate control signal
CalcControl(do_control, do_estimation, do_lock_control, sigma_soft_start,
sigma_u_noise, do_reset_at_control_onset);
return u_return_;
}
template <typename System>
inline void Controller<System>::CalcControl(bool do_control, bool do_estimation,
bool do_lock_control,
data_t sigma_soft_start,
data_t sigma_u_noise,
bool do_reset_at_control_onset) {
if (do_control && do_estimation) {
if (!do_control_prev_) {
if (do_reset_at_control_onset) {
Reset();
}
t_since_control_onset_ = 0.0;
} else {
t_since_control_onset_ += sys_.dt();
}
// enforce softstart on control vars.
if (sigma_soft_start > 0) {
// half-Gaussian soft-start scaling factor
data_t soft_start_sf = 1 - exp(-pow(t_since_control_onset_, 2) /
(2 * pow(sigma_soft_start, 2)));
u_ref_ *= soft_start_sf;
// TODO(mfbolus): May be appropriate to soft-start x_ref, y_ref too
// x_ref_ *= soft_start_sf;
// cx_ref_ *= soft_start_sf;
// y_ref_ *= soft_start_sf;
}
if (!do_lock_control) {
// first do u -> v change of vars. (v = g.*u)
// e.g., convert into physical units (e.g., v[=] mW/mm2 rather than driver
// control voltage u[=]V)
v_ref_ = g_design_ % u_ref_;
// Given FB, calc. the change in control
if (control_type_ & kControlTypeDeltaU) {
// if control designed to minimize not u but deltaU (i.e. state aug with
// u):
// TODO(mfbolus): Commented out for now. See note below.
// du_ref_ = u_ref_ - u_ref_prev_;
// dv_ref_ = g_design_ % du_ref_;
// TODO(mfbolus): Assuming users want *smooth* control signals if using
// kControlTypeDeltaU, it should be the case that dv_ref_ is --> 0. May
// want to revisit, but I am going to force it to be zero unless a
// situation arises that argues for keeping the above.
dv_ref_.zeros();
dv_ = dv_ref_; // nominally-optimal.
dv_ -= Kc_ * (sys_.x() - x_ref_); // instantaneous state error
dv_ -= Kc_u_ * (v_ - v_ref_); // penalty on amp u (rel to ref)
if (control_type_ & kControlTypeIntY) {
// TODO(mfbolus): one approach to protection against integral windup
// would be to not integrate error when control signal saturated:
// if(!uSaturated)
int_e_ += (sys_.cx() - cx_ref_) * sys_.dt(); // integrated error
dv_ -= Kc_inty_ * int_e_; // control for integrated error
}
// update the control
v_ += dv_;
} else {
v_ = v_ref_; // nominally-optimal.
v_ -= Kc_ * (sys_.x() - x_ref_); // instantaneous state error
if (control_type_ & kControlTypeIntY) {
// TODO(mfbolus): one approach to protection against integral windup
// would be to not integrate error when control signal saturated:
// if (!uSaturated)
int_e_ += (sys_.cx() - cx_ref_) * sys_.dt(); // integrated error
v_ -= Kc_inty_ * int_e_; // control for integrated error
}
}
// convert back to control voltage u[=]V
u_ = v_ / sys_.g();
} // else do nothing until lock is low
} else { // if not control
// feed through u_ref in open loop
u_ = u_ref_ % g_design_ / sys_.g();
v_ = sys_.g() % u_;
u_ref_.zeros();
int_e_.zeros();
int_e_awu_adjust_.zeros();
u_sat_.zeros();
} // ends do_control
// enforce box constraints (and antiwindup)
AntiWindup();
// add noise to input?
// The value for u that is *returned* to user after addition of any noise,
// while keeping controller/estimator blind to this addition.
u_return_ = u_;
if ((sigma_u_noise > 0.0) && (do_control && !do_lock_control)) {
u_return_ += sigma_u_noise * Vector(sys_.n_u(), fill::randn);
Limit(u_return_, u_lb_, u_ub_);
};
// For next time step:
u_ref_prev_ = u_ref_;
do_control_prev_ = do_control;
do_lock_control_prev_ = do_lock_control;
} // CalcControl
template <typename System>
inline void Controller<System>::CalcSteadyStateSetPoint() {
// Linearly-constrained least squares (ls).
//
// _reference:
// Boyd & Vandenberghe (2018) Introduction to Applied Linear Algebra
//
Matrix a_ls =
join_horiz(sys_.C(), Matrix(sys_.n_y(), sys_.n_u(), fill::zeros));
Vector b_ls = cx_ref_;
Matrix c_ls = join_horiz(sys_.A() - Matrix(sys_.n_x(), sys_.n_x(), fill::eye),
sys_.B() * arma::diagmat(sys_.g()));
Vector d_ls = -sys_.m0();
if (control_type_ & kControlTypeAdaptM) {
d_ls = -sys_.m(); // adapt setpoint calc with disturbance?
}
Matrix a_ls_t = a_ls.t(); // TODO(mfbolus): not sure why but causes seg
// fault if I do not do this.
Matrix phi_ls =
join_vert(join_horiz(2 * a_ls_t * a_ls, c_ls.t()),
join_horiz(c_ls, Matrix(sys_.n_x(), sys_.n_x(), fill::zeros)));
// TODO(mfbolus): should be actual inverse, rather than pseudo-inverse:
Matrix inv_phi = pinv(phi_ls);
Vector xulam = inv_phi * join_vert(2 * a_ls_t * b_ls, d_ls);
x_ref_ = xulam.subvec(0, sys_.n_x() - 1);
u_ref_ = xulam.subvec(sys_.n_x(), sys_.n_x() + sys_.n_u() - 1);
cx_ref_ = sys_.C() * x_ref_;
} // CalcSteadyStateSetPoint
template <typename System>
void Controller<System>::AntiWindup() {
u_saturated_ = false;
u_sat_ = u_;
// limit u and flag whether saturated
for (size_t k = 0; k < u_.n_elem; k++) {
if (u_[k] < u_lb_) {
u_sat_[k] = u_lb_;
u_saturated_ = true;
}
if (u_[k] > u_ub_) {
u_sat_[k] = u_ub_;
u_saturated_ = true;
}
}
if ((control_type_ & kControlTypeIntY) && (tau_awu_ < lds::kInf)) {
// one-step back-calculation (calculate intE for u=u_sat)
// (Astroem, Rundqwist 1989 warn against using this...)
// int_e_awu_adjust_ =
// solve(Kc_inty_, (u_ - u_sat_)); // pinv(Kc_inty) * (u-uSat);
// gradual: see Astroem, Rundqwist 1989
// this is a fudge for doing MIMO gradual
// n.b., went ahead and multiplied 1/T by dt so don't have to do that here.
int_e_awu_adjust_ =
k_awu_ * (sign(Kc_inty_).t() / sys_.n_u()) * (u_ - u_sat_);
// int_e_awu_adjust_ = k_awu_ * (u_-u_sat_);
int_e_ += int_e_awu_adjust_;
}
// set u to saturated version
u_ = u_sat_;
}
template <typename System>
void Controller<System>::InitVars(size_t control_type) {
// initialize to default values
u_ref_ = Vector(sys_.n_u(), fill::zeros);
u_ref_prev_ = Vector(sys_.n_u(), fill::zeros);
x_ref_ = Vector(sys_.n_x(), fill::zeros);
y_ref_ = Vector(sys_.n_y(), fill::zeros);
cx_ref_ = Vector(sys_.n_y(), fill::zeros);
u_ = Vector(sys_.n_u(), fill::zeros);
u_return_ = Vector(sys_.n_u(), fill::zeros);
u_sat_ = Vector(sys_.n_u(), fill::zeros);
// Might not need all these, so zero elements until later.
Kc_ = Matrix(sys_.n_u(), sys_.n_x(), fill::zeros);
Kc_u_ = Matrix(0, 0, fill::zeros);
Kc_inty_ = Matrix(0, 0, fill::zeros);
g_design_ = sys_.g(); // by default, same as model
dv_ = Vector(sys_.n_u(), fill::zeros);
v_ = Vector(sys_.n_u(), fill::zeros);
du_ref_ = Vector(sys_.n_u(), fill::zeros);
dv_ref_ = Vector(sys_.n_u(), fill::zeros);
v_ref_ = Vector(sys_.n_u(), fill::zeros);
int_e_ = Vector(0, fill::zeros);
int_e_awu_adjust_ = Vector(0, fill::zeros);
set_control_type(control_type);
}
} // namespace lds
#endif
Updated on 19 May 2022 at 17:16:05 Eastern Daylight Time