SOLPS2ctrl.jl
- SOLPS2ctrl.jl
This repository serves as the top most workflow manager with helpful utilities to use other repositories in this project. Following steps are supported/planned in near future:
- Loading SOLPS outputs into IMAS data dictionary format
- Loading equilibrium (that the SOLPS mesh was based on) into IMAS DD format
- Extrapolating results into core and far SOL region if required
- Running synthetic diagnostics on them
- Performing system identification (to be added)
- Designing and tuning linear causal and model predictive controllers (to be added)
Documentation of other repositories in this project
IMASggd.jl
SOLPS2imas.jl
FusionSyntheticDiagnostics.jl
Installation
using Pkg
Pkg.add("SOLPS2ctrl")
SOLPS related functionality
Top file handling functions
SOLPS2ctrl.find_files_in_allowed_folders
— Functionfind_files_in_allowed_folders(
input_dirs::String...;
eqdsk_file::String,
recursive::Bool=true,
)
Searches a list of allowed folders for a set of filenames that will provide information about the SOLPS case. Returns a list of filenames with complete paths.
Example:
SOLPS2ctrl.find_files_in_allowed_folders(
"<your samples folder>/D3D_Ma_184833_03600";
eqdsk_file="g184833.03600",
)
SOLPS2ctrl.geqdsk_to_imas!
— Functiongeqdsk_to_imas!(
eqdsk_file::String,
dd::IMAS.dd;
set_time::Union{Nothing, Float64}=nothing,
time_index::Int=1,
)
Transfers the equilibrium reconstruction from an EFIT-style gEQDSK file into the IMAS DD structure.
SOLPS2ctrl.preparation
— Functionpreparation(
eqdsk_file::String,
dirs::String...;
core_method::String="simple",
filename::String="sd_input_data",
output_format::String="json",
eqdsk_set_time::Union{Nothing, Float64}=nothing,
eq_time_index::Int=1,
allow_boundary_flux_correction::Bool=false,
)::IMAS.dd
Gathers SOLPS and EFIT files and loads them into IMAS structure. Extrapolates profiles as needed to get a complete picture.
Repairing/filling out partial equilibrium files
Tools for repairing/filling out partial equilibrium files.
Some of the added fields may not be totally accurate, so it is recommended to use this tool mainly for test cases, as a utility. For a real equilibrium, problems should be fixed properly.
SOLPS2ctrl.add_rho_to_equilibrium!
— Functionfunction add_rho_to_equilibrium(dd:IMAS.dd)
Adds equilibrium rho profile to the DD
SOLPS2ctrl.check_rho_1d
— Functioncheck_rho_1d(
dd::IMAS.dd;
time_slice::Int=1,
throw_on_fail::Bool=false,
)::Bool
Checks to see if rho exists and is valid in the equilibrium 1d profiles
Extrapolations
Utilities for extrapolating profiles
Core profile extrapolations
SOLPS2ctrl.extrapolate_core
— Functionextrapolate_core(
edge_rho::Vector{Float64},
edge_quantity::Vector{Float64},
rho_output::Vector{Float64},
)::Vector{Float64}
Function for assuming a core profile when given edge profile data.
Concept:
- value and derivative should be continuous when joining real data
- derivative at magnetic axis is known to be 0 when making profile vs. rho, by the definition of rho
- derivative probably does something fancier between the pedestal and axis than just linear interpolation, so add an extra point in there
- there's a joint between the steep pedestal and the shallow core that needs an extra knot to manage it properly
- after making up a very simple gradient profile out of a few line segments, integrate it to get the profile of the quantity in question
SOLPS2ctrl.fill_in_extrapolated_core_profile!
— Functionfill_in_extrapolated_core_profile!(
dd::IMAS.dd,
quantity_name::String;
method::String="simple",
eq_time_idx::Int=1,
eq_profiles_2d_idx::Int=1,
value_field::Symbol=:values,
grid_ggd_idx::Int=1,
cell_subset_idx::Int=5,
)
This function accepts a DD that should be populated with equilibrium
and edge_profiles
as well as a request for a quantity to extrapolate into the core. It then maps edge_profiles
data to rho, calls the function that performs the extrapolation (which is not a simple linear extrapolation but has some trickery to attempt to make a somewhat convincing profile shape), and writes the result to core_profiles. This involves a bunch of interpolations and stuff.
Input arguments:
dd
: an IMAS data dictionaryquantity_name
: the name of a quantity inedge_profiles.profiles_2d
andcore_profiles.profiles_1d
, such as "electrons.density"method
: Extrapolation method.eq_time_id
x: index of the equilibrium time slice to use. For a typical SOLPS run, the SOLPS mesh will be based on the equilibrium reconstruction at a single time, so the DD associated with the SOLPS run only needs one equilibrium time slice to be loaded. However, one could combine the complete equilibrium time series with the SOLPS run and then have to specify which slice of the equilibrium corresponds to the SOLPS mesh.eq_profiles_2d_idx
: index of theprofiles_2D
in equilibriumtime_slice
.value_field
: Symbolic name of the values field in quantity.grid_ggd_idx
: index of thegrid_ggd
to use. For a typical SOLPS run, the SOLPS grid is fixed, so this index defaults to 1. But in future, if a time varying grid is used, then this index will need to be specified.cell_subset_idx
: index of the subset of cells to use for the extrapolation. The default is 5, which is the subset of all cells. Ifedge_profiles
data is instead present for a different subset, for instance, -5, which are b2.5 cells only, then this index should be set to -5.
Edge profiles extrapolations
These functions have not been fully tested and/or supported yet.
SOLPS2ctrl.mesh_psi_spacing
— Functionmesh_psi_spacing(
dd::IMAS.dd;
eq_time_idx::Int=1,
eq_profiles_2d_idx::Int=1,
grid_ggd_idx::Int=1,
space_idx::Int=1,
avoid_guard_cell::Bool=true,
spacing_rule="mean",
)
Inspects the mesh to see how far apart faces are in psi_N. Requires that GGD and equilibrium are populated.
Input Arguments:
dd
: a data dictionary instance with required data loaded into iteq_time_idx
: index of the equilibrium time slice to use. For a typical SOLPS run, the SOLPS mesh will be based on the equilibrium reconstruction at a single time, so the DD associated with the SOLPS run only needs one equilibrium time slice to be loaded. However, one could combine the complete equilibrium time series with the SOLPS run and then have to specify which slice of the equilibrium corresponds to the SOLPS mesh.eq_profiles_2d_id
x: index of theprofiles_2D
in equilibriumtime_slice
.grid_ggd_idx
: index of thegrid_ggd
to use. For a typical SOLPS run, the SOLPS grid is fixed, so this index defaults to 1. But in future, if a time varying grid is used, then this index will need to be specified.space_idx
: index of the space to use. For a typical SOLPS run, there will be only one space so this index will mostly remain at 1.avoid_guard_cell
: assume that the last cell is a guard cell so takeend-2
andend-1
instead ofend
andend-1
spacing_rule
: "edge" or "mean" to make spacing of new cells (inpsi_N
) be the same as the spacing at the edge of the mesh, or the same as the average spacing
SOLPS2ctrl.cached_mesh_extension!
— Functioncached_mesh_extension!(
dd::IMAS.dd,
eqdsk_file::String,
b2fgmtry::String;
eq_time_idx::Int=1,
eq_profiles_2d_idx::Int=1,
grid_ggd_idx::Int=1,
space_idx::Int=1,
clear_cache::Bool=false,
)::String
Adds an extended mesh to a data dictionary, possibly from a cached result.
Input Arguments:
dd
: The data dictionary. It will be modified in place.eqdsk_file
: the name of the EQDSK file that was used to get equilibrium data in the dd.b2fgmtry
: the name of the SOLPS geometry file that was used to get GGD info inedge_profiles
in the dd.eq_time_idx
: Index of the time slice in equilibriumeq_profiles_2d_idx
: Index of the 2D profile set in equilibrium (there is usually only one)grid_ggd_idx
: Index of thegrid_ggd
set in edge_profilesspace_idx
: Index of the spaceclear_cache
: delete any existing cache file (for use in testing)
Unit conversion utilities
SOLPS2ctrl.gas_unit_converter
— Functiongas_unit_converter(
value_in::Float64,
units_in::String,
units_out::String;
species::String="H",
temperature::Float64=293.15,
)
Converts gas flows between different units. Uses ideal gas law to convert between Pressure * volume type flows / quantities and count / current types of units. There is a version that accepts floats in and outputs floats, and another that deals in Unitful quantities.
gas_unit_converter(
value_in::Unitful.Quantity,
units_in::String,
units_out::String;
species::String="H",
temperature=293.15 * Unitful.K,
)
Converts gas flows between different units. Uses ideal gas law to convert between Pressure * volume type flows / quantities and count / current types of units. This is the Unitful version.
Output will be unitful, but the units are not simplified automatically. You can perform operations such as
(output |> Unitful.upreferred).val
Unitful.uconvert(Unitful.whatever, output).val
to handle simplification or conversion of units.
Although this function pretends torr L s$^{-1}$ and Pa m$^3$ s$^{-1}$ are different, use of Unitful should cause them to behave the same way as long as you simplify or convert units at the end. This means that you can use other pressure*volume type gas units and call them torr L s$^{-1}$ and the script will deal with them up to having messy units in the output.
Control related functionality
This package provides a platform to perform closed loop simulations of controllers with plant models and actuator models. The key function to run these simulations is run_closed_loop_sim()
described below. The common architecture for this platform is to create mutable struct
for each of the actuator, plant, and controller which are subtypes of provided abstract types Actuator
, Plant
, and Controller
and the struct itself is callable and performs the required function for a discrete time instance. See test examples to get an idea of how to use this feature.
Plant
SOLPS2ctrl.Plant
— TypePlant
Abstract parent type for all plants. Whenever a user defined plant is created, it must be subtype of Plant
.
To create a new plant with custom feautres, it must be defined as a mutable stucture which is daughter of Plant
that contains all settings and state information for the plant and the instance itself should be callable to take as input a Vector{Float64}
and should return an output of Vector{Float64}
.
mutable struct CustomPlant <: Plant
settings
state
# ... Anything more
end
function (cp::CustomPlant)(inp::Vector{Float64})::Vector{Float64}
# perform the plant single step forward calcualtion with inp
# update cp.state if required
return output
end
NOTE: If you need to add a linear system in your plant model, add a LinearPlant
instance in the attributes of your CustomPlant
and just call the LinearPlant inside your function call.
SOLPS2ctrl.LinearPlant
— TypeLinearPlant
Implementation of linear system with option of scaling and offseting inputs and outputs. It stores the system in sys
and state of the system in x
. Constructor:
LinearPlant(
sys::Union{PredictionStateSpace, StateSpace},
x=zeros(Float64, size(sys.A, 1));
inp_offset::Float64=0.0, inp_factor::Float64=1.0,
out_offset::Float64=0.0, out_factor::Float64=1.0,
)
Creates a LinearPlant instance with state space system sys
and state vector x
. It also defined input offsetting and scaling with inp_offset
and inp_factor
, and similarly output unscaling and unoffseting with out_offset
and out_factor
.
SOLPS2ctrl.InputConditioning
— TypeInputConditioning
Abstract parent type for creating input conditioning to plants. Whenever a user defined input coniditioning is created it must be subtype of InputConditioning
.
To create a new input conditioning with custom feautres, it must be defined as a mutable stucture which is daughter of InputConditioning
that contains all settings and state information for the function and the instance itself should be callable to take as input a Vector{Float64}
and kwargs for any parameters used and should return an output of Vector{Float64}
which is same size as the input.
mutable struct CustomInpCond <: InputConditioning
settings
state
# ... Anything more
end
function (inp_cond::CustomInpCond)(inp::Vector{Float64}; kwargs...)::Vector{Float64}
# perform the input coniditioning single step forward calcualtion with inp
# Use parameters from kwargs
# update inp_cond.state if required
return output
end
Note that inp_cond
must have a call signature of inp_cond(inp::Vector{Float64}; kwargs...)::Vector{Float64}
to take input as a vector for multiple inputs at a particular time instance
SOLPS2ctrl.InpCondLinPlant
— TypeInpCondLinPlant
Implementation of a LinearPlant with an added input conditioning which could be used to make it non-linear. It stores the LinearPlant in linear_plant
, the input coniditioning function in inp_cond
and any keyword arguments for the inp_cond
in inp_cond_kwargs
. Constructor:
InpCondLinPlant{T}(
linear_plant::LinearPlant,
inp_cond::InputConditioning
inp_cond_kwargs::Dict{Symbol, T}
) where {T}
Creates a InpCondLinPlant instance. inp_cond_kwargs
can be used to have changeable parameters in the input conditioning which can be used for optimization and fiting.
System identification and modeling
SOLPS2ctrl.system_id
— Functionsystem_id(
inp::Union{Vector{Float64}, Matrix{Float64}},
out::Union{Vector{Float64}, Matrix{Float64}},
tt::Vector{Float64},
order::Int;
inp_offset::Float64=0.0, inp_factor::Float64=1.0,
out_offset::Float64=0.0, out_factor::Float64=1.0,
inp_cond::Union{Nothing, InputConditioning}=nothing,
inp_cond_kwargs::Dict{Symbol, T}=Dict{Symbol, Any}(),
newpem_kwargs::Dict{Symbol, U}=Dict{Symbol, Any}(),
verbose::Bool=false,
) where {T, U}
Perform system identification for a set on input data inp
, output data out
, and time series vector tt
. If there are more than one inputs or outputs, provide them as Matrix with first dimension for ports (input or output) and second dimension for time.
If inp_cond
is provided, it is applied before offsetting and scaling for performing system identification with keywords for this function provided in inp_cond_kwargs
.
This function uses ControlSystemIdentification.newpem to perform the system identification. Any additional keywords for this function should be passed as dictionary in newpem_kwargs
. For advanced use, it is recommended to do system identification directly with newpem
instead of using this function.
Returns a linear state space model of the order
.
SOLPS2ctrl.system_id_optimal_inp_cond
— Functionsystem_id_optimal_inp_cond(
inp::Union{Vector{Float64}, Matrix{Float64}},
out::Union{Vector{Float64}, Matrix{Float64}},
tt::Vector{Float64},
order::Int,
inp_cond::InputConditioning,
inp_cond_args_guess::Dict{Symbol, T};
inp_offset::Float64=0.0, inp_factor::Float64=1.0,
out_offset::Float64=0.0, out_factor::Float64=1.0,
inp_cond_args_lower::Dict{Symbol, V}=Dict{Symbol, Any}(),
inp_cond_args_upper::Dict{Symbol, W}=Dict{Symbol, Any}(),
newpem_kwargs::Dict{Symbol, U}=Dict{Symbol, Any}(),
curve_fit_kwargs::Dict{Symbol, X}=Dict{Symbol, Any}(),
verbose::Bool=false,
) where {T, U, V, W}
Perform system identification for a set on input data inp
, output data out
, and time series vector tt
. If there are more than one inputs or outputs, provide them as Matrix with first dimension for ports (input or output) and second dimension for time.
The inp_cond
is applied before offsetting and scaling for performing system identification. The inp_cond_args_guess
is used as initial keyword arguments that provide the parameters of the inp_cond
. These arguments are then used to find the best fit while iteratively performing system_id
in each step.
This function uses ControlSystemIdentification.newpem to perform the system identification. Any additional keywords for this function should be passed as dictionary in newpem_kwargs
.
This function uses LsqFit.curve_fit to fit the parameters of input conditions along with performing the system identification.
For advanced use, it is recommended to do system identification directly with newpem
and optimize using your favorite fitting method instead of using this function.
Returns a NonLinInpLinearPlant that has stored best fit parameters for the input non-linearity and a best fit linear model.
SOLPS2ctrl.model_evolve
— Functionmodel_evolve(
plant::Plant,
inp::Union{Vector{Float64}, Matrix{Float64}};
x0::Union{Nothing, Vector{Float64}}=nothing,
initialize_x::Bool=false,
)
Evolve a plant model through a time series of inputs. If inp
is a Matrix, the first dimension will hold different inputs and second dimension will be along time. If inp
is a vector, it would be assumed that the model has a single input. If the model also happens to have a single output and inp
is a vector, the returned outputs will be a vector as well. If x0
is not provided, the stored state vector of the plant model will be used for initialization. If initialize_x
is true, then the state vector of the plant model would be initialized to match first input value so that no sudden jumps happen.
Actuators
SOLPS2ctrl.Actuator
— TypeActuator
Abstract parent type for all actuators. Whenever a user defined actuator is created, it must be subtype of Actuator
.
To create a new actuator with custom function, it must be defined as a mutable stucture which is daughter of Actuator that contains all settings and state information for the actuator and the instance itself should be callable to take as input a Vector{Float64}
and should return an output of Vector{Float64}
.
mutable struct CustomActuator <: Actuator
settings
state
# ... Anything more
end
function (ca::CustomActuator)(inp::Vector{Float64})::Vector{Float64}
# perform the actuation calcualtions with inp
# update ca.state if required
return output
end
NOTE: If you need to add a delay in the actuator, add a DelayedActuator
instance in the attributes of your CustomActuator
and just call the DelayedActuator inside your function call.
SOLPS2ctrl.DelayedActuator
— TypeDelayedActuator{U}
Implementation of delayed actuation. It stores delay::Int
for number of time steps of delay and buffer::Queue{U}
which stores the delayed actuations in a queue. Constructor:
DelayedActuator(
delay::Int;
default_output::T=[0.0],
) where {T}
Creates a DelayedActuator{T} instance with delay
and initializes the buffer
pre-filled upto brim with the default_output.
For example:
using SOLPS2ctrl: DelayedActuator
# Actuator with delay of 3 steps
dact = DelayedActuator(3; default_output=[0.0, 0.0])
inp_series = [ones(2) * i * 0.1 for i in 1:6]
for (i, inp) in enumerate(inp_series)
println("Calling $(i)th time, return value is $(dact(inp))")
end
Calling 1th time, return value is [0.0, 0.0]
Calling 2th time, return value is [0.0, 0.0]
Calling 3th time, return value is [0.0, 0.0]
Calling 4th time, return value is [0.1, 0.1]
Calling 5th time, return value is [0.2, 0.2]
Calling 6th time, return value is [0.30000000000000004, 0.30000000000000004]
Controllers
SOLPS2ctrl.Controller
— TypeController
Abstract parent type for all controllers. Whenever a user defined controller is created it must be a subtype of Controller
.
To create a new controller algorithm, it should be defined as a mutable structure that is a daughter of Controller
and should contain all settings and state information to be stored. It must be a callable structure that can use any of the following keyword arguments:
ii::Int
: Iteration indextarget::Matrix{Float64}
: Target waveform (No. of signals x Time steps)plant_inp::Matrix{Float64}
: Inputs to plant (No. of inputs x Time steps)plant_out::Matrix{Float64}
: Outputs from plant (No. of outputs x Time steps)act::Actuator
: Actuator model in the loopinp_feedforward::Matrix{Float64}
: Feedforward input to the plant (No. of inputs x Time steps)kwargs..
: Required to ignore unused keyword arguments
Linear Controller
SOLPS2ctrl.LinearController
— TypeLinearController
Implementation for using any linear controller. It stores ctrl_ss::StateSpace{TE} where {TE <: Discrete}
for storing any linear controller as a discrete state space model using ControlSystemsBase.ss. It also stores the state vector for the state space model as ctrl_x0::Vector{Float64}
. It's call signature is:
(lc::LinearController)(;
ii::Int,
target::Matrix{Float64},
plant_out::Matrix{Float64},
kwargs...,
)::Vector{Float64}
Calcualtes error as target[:, ii] .- plant_out[:, ii]
and runs it through lsim_step()
.
Predicted Variable Linear Controller
SOLPS2ctrl.PVLC
— TypePVLC
# Constructor
PVLC(
ctrl_ss::StateSpace{TE},
ctrl_x0::Vector{Float64},
plant::LinearPlant,
h::Int,
) where {TE <: Discrete}
Implementation of Predicted Variable Linear Controller (PVLC). It stores ctrl_ss::StateSpace{TE} where {TE <: Discrete}
for storing any linear controller as a discrete state space model using ControlSystemsBase.ss. It also stores the state vector for the state space model as ctrl_x0::Vector{Float64}
. Additionally, it stores the plant
, the number of steps of history h
used for state tracking and state prediction matrices Y2x
and U2x
from state_prediction_matrices()
.
This controller has a call signature:
(pvlc::PVLC)(;
ii::Int,
target::Matrix{Float64},
plant_inp::Matrix{Float64},
plant_out::Matrix{Float64},
act::Actuator,
kwargs...,
)::Vector{Float64}
Tracks the state vector of the plant using h
steps of history from plant_inp
and plant_out
and uses it to calculate future output of the plant. It compares it with a future target
value and applies the linear controller ctrl_ss
there.
SOLPS2ctrl.state_prediction_matrices
— Functionstate_prediction_matrices(
sys::Union{PredictionStateSpace, StateSpace},
h::Int=size(sys.A, 1),
)
Calculate state prediction matrices for a linear system sys
given h
steps of input and output history. This function returns two matrices, Y2x
and U2x
that can be used to calculate least square fitted estimate of the current state vector of the system. Y2x
has size: (State size of sys
) x (No. of outputs times h) U2x
has size: (State size of sys
) x (No. of inputs times h) The estimated state vector is obtained by: Y2x * Y + U2x * U where Y is all the h
outputs of system stacked into a single vector. and U is all the h
inputs to the system stacked into a single vector.
State Prediction Matrix Algebra
At step k:
\[\begin{split} y_k &= C x_k + D u_k\\ x_k &= A x_{k-1} + B u_{k-1}\\ x_{k+1} &= A x_{k} + B u_{k} \end{split}\]
Therefore to h steps in history:
For all output estimates:
\[\begin{split} y_k & = C x_k + D u_k \\ & = C (A x_{k-1} + B u_{k-1}) + D u_k \\ & = C (A (A x_{k-2} + B u_{k-2}) + B u_{k-1}) + D u_k \\ & = ... \\ & = C A^{h-1} x_{k-(h-1)} + C A^{h-2} B u_{k-(h-1)} + C A^{h-3} B u_{k-(h-2)} + ... + CAB u_{k-2} + CB u_{k-1} + D u_k \\ \end{split}\]
\[\begin{split} y_k &= C A^{h-1} x_{k-(h-1)} + C A^{h-2} B u_{k-(h-1)} + C A^{h-3} B u_{k-(h-2)} + ... + CAB u_{k-2} + CB u_{k-1} + D u_k \\ y_{k-1} &= C A^{h-2} x_{k-(h-1)} + C A^{h-3} B u_{k-(h-1)} + C A^{h-4} B u_{k-(h-2)} + ... + CAB u_{k-3} + CB u_{k-2} + D u_{k-1} \\ ... &= ... \\ y_{k-i} &= C A^{h-1-i} x_{k-(h-1)} + C A^{h-2-i} B u_{k-(h-1)} + C A^{h-3-i} B u_{k-(h-2)} + ... + CB u_{k-i-1} + D u_{k-i} \\ ... &= ... \\ y_{k-(h-3)} &= C A^2 x_{k-(h-1)} + CAB u_{k-(h-1)}+ CB u_{k-(h-2)} + D u_{k-(h-3)} \\ y_{k-(h-2)} &= C A x_{k-(h-1)} + CB u_{k-(h-1)} + D u_{k-(h-2)} \\ y_{k-(h-1)} &= C x_{k-(h-1)} + D u_{k-(h-1)} \end{split}\]
For predicted next state:
\[\begin{split} x_{k+1} & = A x_{k} + B u_{k} \\ & = A (A x_{k-1} + B u_{k-1}) + B u_{k} \\ & = A (A (A x_{k-2} + B u_{k-2}) + B u_{k-1}) + B u_{k} \\ & = ... \\ & = A^h x_{k-(h-1)} + A^{h-1} B u_{k-(h-1)} + A^{h-2} B u_{k-(h-2)} + ... + A^2 B u_{k-2} + A B u_{k-1} + B u_{k} \\ \end{split}\]
In terms of mega-matrices, define mega-vectors of inputs and outputs:
\[\vec{Y} = \begin{bmatrix} y_{k-(h-1)} \\ y_{k-(h-2)} \\ ... \\ y_{k-1} \\ y_{k} \end{bmatrix}\]
Note that for multiple outputs, each output vector will be stacked vertically to create a single column.
\[\vec{U} = \begin{bmatrix} u_{k-(h-1)} \\ u_{k-(h-2)} \\ ... \\ u_{k-1} \\ u_{k} \end{bmatrix}\]
Note that for multiple inputs, each input vector will be stacked vertically to create a single column.
Then, from $x_{k-(h-1)}$ and $\vec{U}$, we get $\vec{Y}$ and predicted state $x_{k+1}$:
\[\vec{Y} = \mathcal{L} x_{k-(h-1)} + \mathcal{M} \vec{U}\]
\[x_{k+1} = \mathcal{N} x_{k-(h-1)} + \mathcal{O} \vec{U}\]
Where the mega-matrices $\mathcal{L}$, $\mathcal{M}$, $\mathcal{N}$, $\mathcal{O}$ are:
$\mathcal{L}$ is a matrix with (h x no. of outputs) rows and state-space order columns:
\[\mathcal{L} = \begin{bmatrix} C... \\ CA... \\ CA^2... \\ ... \\ CA^{h-2}... \\ CA^{h-1}... \end{bmatrix}\]
$\mathcal{M}$ is a matrix with (h x no. of outputs) rows and (h x no. of inputs) columns:
\[\mathcal{M} = \begin{bmatrix} D & 0 & 0 & ... & 0 & 0 & 0 & 0 & \\ CB & D & 0 & ... & 0 & 0 & 0 & 0 & \\ CAB & CB & D & ... & 0 & 0 & 0 & 0 & \\ ... & ... & ... & ... & ... & ... & ... & ... & ... \\ C A^{h-4} B & C A^{h-5} B & C A^{h-6} B & ... & CAB & CB & D & 0 & 0 \\ C A^{h-3} B & C A^{h-4} B & C A^{h-5} B & ... & CA^2B & CAB & CB & D & 0 \\ C A^{h-2} B & C A^{h-3} B & C A^{h-4} B & ... & CA^3B & CA^2B & CAB & CB & D \\ \end{bmatrix}\]
$\mathcal{N}$ is a square matrix with state-space order columns and rows:
\[\mathcal{N} = A^{h}\]
$\mathcal{O}$ is a matrix with state-space order rows and (h x no. of inputs) columns:
\[\mathcal{O} = \begin{bmatrix} A^{h-1} B & A^{h-2} B & A^{h-3} B & ... & A^2 B & AB & B \end{bmatrix}\]
Then, future state can be predicted by:
\[x_{k+1} = \mathcal{N} \mathcal{L}^{-1} (\vec{Y} - \mathcal{M} \vec{U}) + \mathcal{O} \vec{U}\]
Model Predictive Controller
SOLPS2ctrl.MPC
— TypeMPC
# Constructor
MPC(
plant::Plant,
h::Int,
act::Actuator, # Actuator model without delay
horizon::Int, # Number of steps in future after latency
nopt::Int, # Number of optimization points in horizon window
opt_every::Int; # Run cost optimization every opt_every steps
ctrl_out_bounds::Tuple{Vector{Float64}, Vector{Float64}}=(
Array{Float64}(undef, 0),
Array{Float64}(undef, 0),
),
guess::Union{Symbol, Vector{Float64}}=:zeros,
curve_fit_kwargs::Dict{Symbol, T}=Dict{Symbol, Any}(),
)
Implementation of simple leaqt square optimized Model Predictive Controller (MPC). It stores the plant
, the number of steps of history h
used for state tracking and state prediction matrices Y2x
and U2x
from state_prediction_matrices()
. It stores a current deep copy of actuator instance in act
to try it during optimization and it stores setting for least square optimization. It also stores a future_evolve
that is created based on plant
, act
, and optimization setting and is the model function that is used for optimization later. This controller stores a buffer for control outputs, ctrl_out_buffer
so that it can be called less often and it can reuse it's previous optimization results.
This contructor takes in minimum required information to create a self-consistent MPC instance. It sets the other dependent quantities in MPC such as Y2x
, U2x
, min_delay
, and create a future_evolve
function and initializes the ctrl_out_buffer
. act
needs to be a deepcopy of the actuator instance. horizon
is the number of steps after the min_delay
among all acturators for which the optimization is carried out. nopt
is the number of optimization points taken along the horizon
which are lineary distributed. The gaps between this optimzation points are interpolated linearly in the output. opt_every
defines the frequency of optimization, i.e. at every opt_every
call of this controller, the optimization is carried out. This avoids unnecessary over-calculations and thus results in a faster controller. ctrl_out_bounds
is a tuple of lower bounds and upper bounds for the control output. guess
is used to create the initial guess during least square optimization. If :last
, it would use the last controller output as initial setting. If it is a Vector
, each initialization starts with this Vector. In all other cases, initial guess is zeros. curve_fit_kwargs
can be used to provide keyword arguments that go to curve_fit
.
This controller has a call signature:
function (mpc::MPC)(;
ii::Int,
target::Matrix{Float64},
plant_inp::Matrix{Float64},
plant_out::Matrix{Float64},
act::Actuator,
inp_feedforward::Matrix{Float64}=zeros(
Float64,
(size(get_sys(plant).B, 2), length(target)),
),
kwargs...,
)::Vector{Float64} where {T}
Tracks the state vector of the plant using h
steps of history from plant_inp
and plant_out
and uses it along with act
to run an optimization to match target
in future after the minimum delay from all the actuators. This function uses curve_fit()
for the non-linear least squares fitting which uses Levenberg-Marquardt algorithm. This function performs the optimization every opt_every
call and uses the stored control output in ctrl_out_buffer
meanwhile.
Closed loop simulations
SOLPS2ctrl.run_closed_loop_sim
— Functionrun_closed_loop_sim(
plant::Union{PredictionStateSpace{TE}, StateSpace{TE}},
act::Actuator,
ctrl::Controller,
target::Matrix{Float64};
inp_feedforward::Matrix{Float64}=zeros(
Float64,
(size(get_sys(plant).B, 2), size(target, 2)),
),
ctrl_start_ind::Int=1,
noise_plant_inp::Matrix{Float64}=zeros(
Float64,
(size(get_sys(plant).B, 2), size(target, 2)),
),
noise_plant_out::Matrix{Float64}=zeros(
Float64,
(size(get_sys(plant).C, 1), size(target, 2)),
),
noise_ctrl_out::Matrix{Float64}=zeros(
Float64,
(size(get_sys(plant).B, 2), size(target, 2)),
),
) where {T, TE <: Discrete}
Generic function to run closed loop simulations with provided plant
, actuator act
, controller ctrl
, and target waveform target
. The length of simulation is determined by provided target
. Keyword arguments are possible for providing adjustments to inputs and outputs of the plant model as explained in model_evolve()
. Additionally, ctrl_start_ind
can be provided to start control loop at an arbitrary point in the loop. noise_plant_inp
, noise_plant_out
, and noise_ctrl_out
allow addition of predefined noise waveforms at the input of plant, output of plant, and the output of controller respectively.
Control related utilities
SOLPS2ctrl.lsim_step
— Functionlsim_step(
sys::Union{PredictionStateSpace, StateSpace}, u::Vector{Float64};
x0::Vector{Float64}=zeros(size(sys.A, 1)),
)::Tuple{Vector{Float64}, Vector{Float64}}
Single step version of ControlSystemsBase.lsim
SOLPS2ctrl.offset_scale
— Functionoffset_scale(
val::Union{Float64, Vector{Float64}, Matrix{Float64}};
offset::Union{Float64, Vector{Float64}}=0.0,
factor::Union{Float64, Vector{Float64}}=1.0,
)::typeof(val)
Subtract an offset
and multiply by a factor
, the val
to make it nominally in the range of -1 to 1 (not strictly) for easy identification of system.
(val .- offset) .* factor
SOLPS2ctrl.unscale_unoffset
— Functionunscale_unoffset(
offset_scaled::Union{Float64, Vector{Float64}, Matrix{Float64}};
offset::Union{Float64, Vector{Float64}}=0.0,
factor::Union{Float64, Vector{Float64}}=1.0,
)::typeof(offset_scaled)
Undo previously applied offset and scaling.
offset_scaled ./ factor .+ offset