From 4d82c3c25efbc51caf67054b02f1bc7daa59005d Mon Sep 17 00:00:00 2001 From: rishimandyam Date: Thu, 18 Jan 2024 17:49:50 -0600 Subject: [PATCH 01/12] updated make.jl with new tutorial --- docs/make.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/make.jl b/docs/make.jl index ff089d24..aab2a708 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -18,7 +18,8 @@ makedocs(; "Modeling with OptiGraphs" => "documentation/modeling.md", "Graph Partitioning and Processing" => "documentation/partitioning.md", "Tutorials" => - ["Optimal Control of a Natural Gas Network" => "tutorials/gas_pipeline.md"], + ["Optimal Control of a Natural Gas Network" => "tutorials/gas_pipeline.md", + "Optimal Control of a Quadcopter" => "tutorials/quadcopter.md"], "API Documentation" => "documentation/api_docs.md", ], ) From 9cec57bc8f8356b4541f3e70e57664813c0040ff Mon Sep 17 00:00:00 2001 From: rishimandyam Date: Mon, 22 Jan 2024 19:24:57 -0600 Subject: [PATCH 02/12] added documentation --- docs/src/tutorials/quadcopter.md | 222 +++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 docs/src/tutorials/quadcopter.md diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md new file mode 100644 index 00000000..f3dd372c --- /dev/null +++ b/docs/src/tutorials/quadcopter.md @@ -0,0 +1,222 @@ +# Optimal Control of a Quadcopter + +By: Rishi Mandyam + +This tutorial notebook is an introduction to the graph-based modeling framework +Plasmo.jl (PLatform for Scalable Modeling and Optimization) for JuMP +(Julia for Mathematical Programming). + +To begin we will import and use the neccessary packages as shown in the next +code block. + +``using JuMP`` +``using Plasmo`` +``using Ipopt`` +``using Plots`` +``using LinearAlgebra`` + +Establish Setpoints for each timepoint +``X_ref = 0:10/N:10; + dXdt_ref = 0:10/N:10; + Y_ref = 0:10/N:10; + dYdt_ref = 0:10/N:10; + Z_ref = 0:10/N:10; + dZdt_ref = 0:10/N:10; + g_ref = 0:10/N:10; + b_ref = 0:10/N:10; + a_ref = 0:10/N:10;`` + +Define Vector of setpoints +`` xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref];`` + +Define Constants +``grav = 9.8 + C_a = 1`` + +``Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); + R = diagm([1/10, 1/10, 1/10, 1/10]);`` + +Convert Derivatives using explicit euler's scheme +``xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a]`` + +Transpose Reference Matrix +``xk_ref1 = zeros(N,9) + for i in (1:N) + for j in 1:length(xk_ref) + xk_ref1[i,j] = xk_ref[j][i] + end + end`` + +``uk = [C_a, wx, wy, wz]`` + +``QuadCopter = Model(Ipopt.Optimizer) # Initialize Model Optimizer # why is model here? + graph = OptiGraph() # Initialize Optigraph + set_optimizer_attribute(graph, "max_iter", 100) +`` + +``@optinode(graph, nodes[1:N])`` + + ``for (i, node) in enumerate(nodes) + + # Create Variables in each of the nodes + @variable(node, g) + @variable(node, b) + @variable(node, a) + + @variable(node, X) + @variable(node, Y) + @variable(node, Z) + + @variable(node, dXdt) + @variable(node, dYdt) + @variable(node, dZdt) + + @variable(node, C_a) + + if i == 1 # Set the initial value of each variable to 0 + @constraint(node, X == 0) + @constraint(node, Y == 0) + @constraint(node, Z == 0) + @constraint(node, dXdt == 0) + @constraint(node, dYdt == 0) + @constraint(node, dZdt == 0) + @constraint(node, g == 0) + @constraint(node, b == 0) + @constraint(node, a == 0) + end + + #Establish the variables and nonlinear constraints given in the problem statement + @variable(node, d2Xdt2) + @NLconstraint(node, d2Xdt2 == C_a*(cos(g)*sin(b)*cos(a) + sin(g)*sin(a))) + @variable(node, d2Ydt2) + @NLconstraint(node, d2Ydt2 == C_a*(cos(g)*sin(b)*sin(a) + sin(g)*cos(a))) + @variable(node, d2Zdt2) + @NLconstraint(node, d2Zdt2 == C_a*cos(g)*cos(b) - grav) + + @variable(node, wx) + @variable(node, wy) + @variable(node, wz) + + @variable(node, dgdt) + @NLconstraint(node, dgdt == (wx*cos(g) + wy*sin(g))/(cos(b))) + @variable(node, dbdt) + @NLconstraint(node, dbdt == -wx*sin(g) + wy*cos(g)) + @variable(node, dadt) + @NLconstraint(node, dadt == wx*cos(g)*tan(b) + wy*sin(g)*tan(b) + wz) + + xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables + xk1 = xk-xk_ref1[i,:] # Array to hold the difference between variable values and their setpoints. + + uk = [C_a, wx, wy, wz] + + # Establish objective function given in problem statement + @objective(node, Min, (1/2*(xk1')*Q*(xk1) + 1/2*(uk')*R*(uk)) * dt) #row Q column + + end + + # not a decomposition scheme + # Add link constraints between nodes using explicit Euler's scheme and problem statement + + for i in 1:(N-1) # iterate through each node except the last + + @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) + @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) + @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) + + @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) + @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) + @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) + + @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) + @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) + @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) + + end + # Set Optimizer + set_optimizer(graph, Ipopt.Optimizer); + set_optimizer_attribute(graph, "max_iter", 700); + # Call the optimizer + optimize!(graph); + + return objective_value(graph), nodes, xk_ref; + end`` + +# Obtain Objective Values for Varying Numbers of Nodes + time_steps = 2:4:50 + + N = length(time_steps) + dt = .5 + obj_val_N = zeros(N) + + + for i in 1:length(time_steps) + timing = @elapsed begin + objval, nodes = Quad(time_steps[i], 10 / time_steps[i]); + obj_val_N[i] = objval + end + println("Done with iteration $i after ", timing, " seconds") + end + +Plot the Relationship +``plot(time_steps, obj_val_N, title = "Objective Value vs Number of Nodes (N)", xlabel = "Number of Nodes (N)", ylabel = "Objective Value") +`` + +# Show Graph of Current Value, Control Action, and Setpoint + ``N = 100 + dt = .1 + objv, nodes = Quad(N, dt) + + CAval_array = zeros(length(nodes)) + xval_array = zeros(length(nodes)) + yval_array = zeros(length(nodes)) + zval_array = zeros(length(nodes)) + + for (i, node) in enumerate(nodes) + CAval_array[i] = value(node[:C_a]) + xval_array[i] = value(node[:X]) + yval_array[i] = value(node[:Y]) + zval_array[i] = value(node[:Z]) + end + + xarray = Array{Array}(undef, 2) + xarray[1] = xval_array + xarray[2] = 0:10/(N-1):10 + + yarray = Array{Array}(undef, 2) + yarray[1] = yval_array + yarray[2] = 0:10/(N-1):10 + + zarray = Array{Array}(undef, 2) + zarray[1] = zval_array + zarray[2] = 0:10/(N-1):10 + + print(CAval_array) + +# Show the Position of the Quadcopter in Relation to its Setpoint + +``plot((1:length(xval_array)), xarray[1:end], title = "X value over time", xlabel = "Node (N)", ylabel = "X Value", label = ["Current X position" "X Setpoint"])`` +``plot((1:length(yval_array)), yarray[1:end], title = "Y value over time", xlabel = "Node (N)", ylabel = "Y Value", label = ["Current Y position" "Y Setpoint"])`` +``plot((1:length(zval_array)), zarray[1:end], title = "Z value over time", xlabel = "Node (N)", ylabel = "Z Value", label = ["Current Z position" "Z Setpoint"])`` + +# Obtain Objective Values of Varying Levels of Time Discretization + + ``N = 5 # Number of Nodes + dt_array = .5:.5:3 + obj_val_dt = zeros(length(dt_array)) + + for (i, dt_val) in enumerate(dt_array) + objval, nodes = Quad(N, dt_val); + obj_val_dt[i] = objval + end + +Plot the Relationship +``plot((1:length(obj_val_dt))*dt, obj_val_dt, title = "Objective Value vs dt", xlabel = "dt value", ylabel = "Objective Value", legend = :none, color = :black, linewidth = 2)`` + +# Conclusion +In this tutorial you were able to: +- successfully used model predictive control to manipulate the postion of a quadcopter +- Show graphical relationships between significant varables. + +Next Steps: +- Try adjusting the initial conditions to see how the behavior of the quadcopter changes! + From 1faa7a6b2c3aa2ebe9ee2ff8e2fa1c7af1e4ed95 Mon Sep 17 00:00:00 2001 From: rishimandyam Date: Wed, 3 Jul 2024 16:28:29 -0500 Subject: [PATCH 03/12] quadcopter changes --- docs/src/tutorials/quadcopter.md | 495 +++++++++++++++++++------------ 1 file changed, 300 insertions(+), 195 deletions(-) diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index f3dd372c..12fb6402 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -6,217 +6,322 @@ This tutorial notebook is an introduction to the graph-based modeling framework Plasmo.jl (PLatform for Scalable Modeling and Optimization) for JuMP (Julia for Mathematical Programming). -To begin we will import and use the neccessary packages as shown in the next -code block. - -``using JuMP`` -``using Plasmo`` -``using Ipopt`` -``using Plots`` -``using LinearAlgebra`` - -Establish Setpoints for each timepoint -``X_ref = 0:10/N:10; - dXdt_ref = 0:10/N:10; - Y_ref = 0:10/N:10; - dYdt_ref = 0:10/N:10; - Z_ref = 0:10/N:10; - dZdt_ref = 0:10/N:10; - g_ref = 0:10/N:10; - b_ref = 0:10/N:10; - a_ref = 0:10/N:10;`` - -Define Vector of setpoints -`` xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref];`` - -Define Constants -``grav = 9.8 - C_a = 1`` - -``Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); - R = diagm([1/10, 1/10, 1/10, 1/10]);`` - -Convert Derivatives using explicit euler's scheme -``xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a]`` +The following problem comes from the paper of Na, Shin, Anitescu, and Zavala (available [here](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9840913)). + +A quadcopter operates in 3-D space with positions $(x, y, z)$ and angles ($\gamma$, $\beta$, and $\alpha$). +$g$ is the graviational constant. The set of state variables at time $t$ are treated as $\boldsymbol{x}_t = (x, y, z, \dot{x}, \dot{y}, \dot{z}, \gamma, \beta, \alpha)$. +The input variables at time $t$ are $\boldsymbol{u}_t = (a, \omega_x, \omega_y, \omega_z)$. +The quadcopter operates according to the constraints + +### 1. To begin we will import and use the necessary packages + +```julia +using JuMP +using Plasmo +using Ipopt +using Plots +using LinearAlgebra +``` + +### 2. Lets Design our function + +This function will: + +The function inputs are: +- number of nodes (N) +- time discretization (number of seconds between nodes [dt]) + +The function outputs are: +- The objective value of the solved graph which is: + - The sum of all variables in all nodes of the graph +- an array containing each node in the graph (nodes) +- an array with the reference values on each node (xk_ref) + +```julia + function Quad(N, dt) +``` + + Establish the setpoints for each timepoint + In this example the quadcopter will fly in a linear upward path + in the positive X, Y, and Z directions +```julia =# + X_ref = 0:10/N:10; + dXdt_ref = 0:10/N:10; + Y_ref = 0:10/N:10; + dYdt_ref = 0:10/N:10; + Z_ref = 0:10/N:10; + dZdt_ref = 0:10/N:10; + g_ref = 0:10/N:10; + b_ref = 0:10/N:10; + a_ref = 0:10/N:10; +``` +Define the vector of setpoints + +$\boldsymbol{x}_t = (x, y, z, \dot{x}, \dot{y}, \dot{z}, \gamma, \beta, \alpha)$ + +```julia + xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref]; +``` +Define Constants and arrays +```julia + grav = 9.8 # m/s^2 + + Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); + R = diagm([1/10, 1/10, 1/10, 1/10]); +``` Transpose Reference Matrix -``xk_ref1 = zeros(N,9) - for i in (1:N) - for j in 1:length(xk_ref) - xk_ref1[i,j] = xk_ref[j][i] - end - end`` - -``uk = [C_a, wx, wy, wz]`` - -``QuadCopter = Model(Ipopt.Optimizer) # Initialize Model Optimizer # why is model here? - graph = OptiGraph() # Initialize Optigraph - set_optimizer_attribute(graph, "max_iter", 100) -`` - -``@optinode(graph, nodes[1:N])`` - - ``for (i, node) in enumerate(nodes) - - # Create Variables in each of the nodes - @variable(node, g) - @variable(node, b) - @variable(node, a) - - @variable(node, X) - @variable(node, Y) - @variable(node, Z) - - @variable(node, dXdt) - @variable(node, dYdt) - @variable(node, dZdt) - - @variable(node, C_a) - - if i == 1 # Set the initial value of each variable to 0 - @constraint(node, X == 0) - @constraint(node, Y == 0) - @constraint(node, Z == 0) - @constraint(node, dXdt == 0) - @constraint(node, dYdt == 0) - @constraint(node, dZdt == 0) - @constraint(node, g == 0) - @constraint(node, b == 0) - @constraint(node, a == 0) - end - - #Establish the variables and nonlinear constraints given in the problem statement - @variable(node, d2Xdt2) - @NLconstraint(node, d2Xdt2 == C_a*(cos(g)*sin(b)*cos(a) + sin(g)*sin(a))) - @variable(node, d2Ydt2) - @NLconstraint(node, d2Ydt2 == C_a*(cos(g)*sin(b)*sin(a) + sin(g)*cos(a))) - @variable(node, d2Zdt2) - @NLconstraint(node, d2Zdt2 == C_a*cos(g)*cos(b) - grav) - - @variable(node, wx) - @variable(node, wy) - @variable(node, wz) - - @variable(node, dgdt) - @NLconstraint(node, dgdt == (wx*cos(g) + wy*sin(g))/(cos(b))) - @variable(node, dbdt) - @NLconstraint(node, dbdt == -wx*sin(g) + wy*cos(g)) - @variable(node, dadt) - @NLconstraint(node, dadt == wx*cos(g)*tan(b) + wy*sin(g)*tan(b) + wz) - - xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables - xk1 = xk-xk_ref1[i,:] # Array to hold the difference between variable values and their setpoints. - - uk = [C_a, wx, wy, wz] - - # Establish objective function given in problem statement - @objective(node, Min, (1/2*(xk1')*Q*(xk1) + 1/2*(uk')*R*(uk)) * dt) #row Q column - - end - - # not a decomposition scheme - # Add link constraints between nodes using explicit Euler's scheme and problem statement - - for i in 1:(N-1) # iterate through each node except the last - - @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) - @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) - @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) - - @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) - @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) - @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) - - @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) - @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) - @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) - - end - # Set Optimizer - set_optimizer(graph, Ipopt.Optimizer); - set_optimizer_attribute(graph, "max_iter", 700); - # Call the optimizer - optimize!(graph); - - return objective_value(graph), nodes, xk_ref; - end`` - -# Obtain Objective Values for Varying Numbers of Nodes - time_steps = 2:4:50 - - N = length(time_steps) - dt = .5 - obj_val_N = zeros(N) +```Julia + xk_ref1 = zeros(N,9) + for i in (1:N) + for j in 1:length(xk_ref) + xk_ref1[i,j] = xk_ref[j][i] + end + end + + uk = [C_a, wx, wy, wz] +``` + +Initialize the model optimizer +```Julia + QuadCopter = Model(Ipopt.Optimizer) +``` + +Create the optigraph +```Julia + graph = OptiGraph() +``` + +Set the maximum amount of iterations for the optimizer. +```julia + set_optimizer_attribute(graph, "max_iter", 100) +``` +Create N optinodes on the graph +```julia + @optinode(graph, nodes[1:N]) +``` + +Add the function variables and constraints to each node in the graph +```julia + for (i, node) in enumerate(nodes) + # Create Variables in each of the nodes + @variable(node, g) + @variable(node, b) + @variable(node, a) - for i in 1:length(time_steps) - timing = @elapsed begin - objval, nodes = Quad(time_steps[i], 10 / time_steps[i]); - obj_val_N[i] = objval - end - println("Done with iteration $i after ", timing, " seconds") - end - -Plot the Relationship -``plot(time_steps, obj_val_N, title = "Objective Value vs Number of Nodes (N)", xlabel = "Number of Nodes (N)", ylabel = "Objective Value") -`` - -# Show Graph of Current Value, Control Action, and Setpoint - ``N = 100 - dt = .1 - objv, nodes = Quad(N, dt) + @variable(node, X) + @variable(node, Y) + @variable(node, Z) - CAval_array = zeros(length(nodes)) - xval_array = zeros(length(nodes)) - yval_array = zeros(length(nodes)) - zval_array = zeros(length(nodes)) + @variable(node, dXdt) + @variable(node, dYdt) + @variable(node, dZdt) - for (i, node) in enumerate(nodes) - CAval_array[i] = value(node[:C_a]) - xval_array[i] = value(node[:X]) - yval_array[i] = value(node[:Y]) - zval_array[i] = value(node[:Z]) - end + @variable(node, C_a) + + if i == 1 # Set the initial value of each variable to 0 + @constraint(node, X == 0) + @constraint(node, Y == 0) + @constraint(node, Z == 0) + @constraint(node, dXdt == 0) + @constraint(node, dYdt == 0) + @constraint(node, dZdt == 0) + @constraint(node, g == 0) + @constraint(node, b == 0) + @constraint(node, a == 0) + end +``` + +Establish the variables, nonlinear constraints, and objective functions given in the problem statement + +\begin{align*} +\frac{d^2x}{dt^2} &= a (\cos\gamma \sin \beta \cos \alpha + \sin \gamma \sin \alpha) \\ +\frac{d^2 y}{dt^2} &= a (\cos \gamma \sin \beta \sin \alpha - \sin \gamma \cos \alpha) \\ +\frac{d^2 z}{dt^2} &= a \cos \gamma \cos \beta - g \\ +\frac{d\gamma}{dt} &= (\omega_x \cos \gamma + \omega_y \sin \gamma) / \cos \beta\\ +\frac{d\beta}{dt} &= -\omega_x \sin \gamma + \omega_y \cos \gamma \\ +\frac{d\alpha}{dt} &= \omega_x \cos \gamma \tan \beta + \omega_y \sin \gamma \tan \beta + \omega_z +\end{align*} + +The input variables at time $t$ are $\boldsymbol{u}_t = (a, \omega_x, \omega_y, \omega_z)$ + +```julia + @variable(node, d2Xdt2) + @NLconstraint(node, d2Xdt2 == C_a*(cos(g)*sin(b)*cos(a) + sin(g)*sin(a))) + @variable(node, d2Ydt2) + @NLconstraint(node, d2Ydt2 == C_a*(cos(g)*sin(b)*sin(a) + sin(g)*cos(a))) + @variable(node, d2Zdt2) + @NLconstraint(node, d2Zdt2 == C_a*cos(g)*cos(b) - grav) + + @variable(node, wx) + @variable(node, wy) + @variable(node, wz) + + @variable(node, dgdt) + @NLconstraint(node, dgdt == (wx*cos(g) + wy*sin(g))/(cos(b))) + @variable(node, dbdt) + @NLconstraint(node, dbdt == -wx*sin(g) + wy*cos(g)) + @variable(node, dadt) + @NLconstraint(node, dadt == wx*cos(g)*tan(b) + wy*sin(g)*tan(b) + wz) +``` +```julia + xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables + xk1 = xk-xk_ref1[i,:] # Array to hold the difference between variable values and their setpoints. + + uk = [C_a, wx, wy, wz] +``` +Establish the objective function. This is the same as the stage cost function given in problem statement. + +$\phi := \frac{1}{2} (\boldsymbol{x}_t - \boldsymbol{x}^{ref}_t)^\top Q (\boldsymbol{x} - \boldsymbol{x}^{ref}_t) + \boldsymbol{u}^\top R \boldsymbol{u}$ +where +$\boldsymbol{x}^{ref}_t$ are the reference values at time $t$. + +```julia + + @objective(node, Min, (1/2*(xk1')*Q*(xk1) + 1/2*(uk')*R*(uk)) * dt) #row Q column + + end +``` +Add link constraints between nodes using the explicit Euler's scheme. +```julia + for i in 1:(N-1) # iterate through each node except the last - xarray = Array{Array}(undef, 2) - xarray[1] = xval_array - xarray[2] = 0:10/(N-1):10 + @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) + @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) + @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) - yarray = Array{Array}(undef, 2) - yarray[1] = yval_array - yarray[2] = 0:10/(N-1):10 + @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) + @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) + @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) - zarray = Array{Array}(undef, 2) - zarray[1] = zval_array - zarray[2] = 0:10/(N-1):10 + @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) + @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) + @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) - print(CAval_array) - -# Show the Position of the Quadcopter in Relation to its Setpoint - -``plot((1:length(xval_array)), xarray[1:end], title = "X value over time", xlabel = "Node (N)", ylabel = "X Value", label = ["Current X position" "X Setpoint"])`` -``plot((1:length(yval_array)), yarray[1:end], title = "Y value over time", xlabel = "Node (N)", ylabel = "Y Value", label = ["Current Y position" "Y Setpoint"])`` -``plot((1:length(zval_array)), zarray[1:end], title = "Z value over time", xlabel = "Node (N)", ylabel = "Z Value", label = ["Current Z position" "Z Setpoint"])`` + end +``` -# Obtain Objective Values of Varying Levels of Time Discretization +Set and call the optimizer +```julia - ``N = 5 # Number of Nodes - dt_array = .5:.5:3 - obj_val_dt = zeros(length(dt_array)) - - for (i, dt_val) in enumerate(dt_array) - objval, nodes = Quad(N, dt_val); - obj_val_dt[i] = objval + set_optimizer(graph, Ipopt.Optimizer); + set_optimizer_attribute(graph, "max_iter", 700); + + optimize!(graph); end - -Plot the Relationship -``plot((1:length(obj_val_dt))*dt, obj_val_dt, title = "Objective Value vs dt", xlabel = "dt value", ylabel = "Objective Value", legend = :none, color = :black, linewidth = 2)`` - -# Conclusion +``` +Now that we have created our function to model the behavior of the quadcopter, +we can test it using some example cases. + +### Example: +- N = 50 time points +- dt = 0.1 seconds +```julia +N = 100 +dt = .1 +objv, nodes, xk_ref = Quad(N, dt) + +# create empty arrays +CAval_array = zeros(length(nodes)) +xval_array = zeros(length(nodes)) +yval_array = zeros(length(nodes)) +zval_array = zeros(length(nodes)) + +# add values to arrays +for (i, node) in enumerate(nodes) + CAval_array[i] = value(node[:C_a]) + xval_array[i] = value(node[:X]) + yval_array[i] = value(node[:Y]) + zval_array[i] = value(node[:Z]) +end + +xarray = Array{Array}(undef, 2) +xarray[1] = xval_array +xarray[2] = 0:10/(N-1):10 + +yarray = Array{Array}(undef, 2) +yarray[1] = yval_array +yarray[2] = 0:10/(N-1):10 + +zarray = Array{Array}(undef, 2) +zarray[1] = zval_array +zarray[2] = 0:10/(N-1):10 +``` +### Let's see the position of the quadcopter in relation to its setpoint in all three dimentions + +```julia +plot((1:length(xval_array)), xarray[1:end], + title = "X value over time", + xlabel = "Node (N)", + ylabel = "X Value", + label = ["Current X position" "X Setpoint"]) +``` +Repeat This process for the Y-position and Z-position + +### Your plots should look something like this + +drawing +drawing +drawing + +### Now that we have solved for the optimal solution, lets explore some other correlations. + +Let's see how increasing the number of nodes changes the objective value of the system +```julia +time_steps = 2:4:50 + +N = length(time_steps) +dt = .5 +obj_val_N = zeros(N) + + +for i in 1:length(time_steps) + timing = @elapsed begin + objval, nodes, xk_ref = Quad(time_steps[i], 10 / time_steps[i]); + obj_val_N[i] = objval + end + println("Done with iteration $i after ", timing, " seconds") +end + +Quad_Obj_NN = plot(time_steps, obj_val_N, + title = "Objective Value vs Number of Nodes (N)", + xlabel = "Number of Nodes (N)", + ylabel = "Objective Value", + label = "Objective Value") +``` + +drawing +The plot shows that as you increase the number of nodes, the objective value of the system decreases. + +Let's see how changing the dt value changes the objective value of the system. + +```julia +N = 5 # Number of Nodes +dt_array = .5:.5:5 +obj_val_dt = zeros(length(dt_array)) + +for (i, dt_val) in enumerate(dt_array) + objval, nodes = Quad(N, dt_val); + obj_val_dt[i] = objval +end + +# use termination_status(graph) + +plot((1:length(obj_val_dt))*dt, obj_val_dt, + title = "Objective Value vs dt", + xlabel = "dt value", + ylabel = "Objective Value", + legend = :none, color = :black, + linewidth = 2) +``` + +### Conclusion In this tutorial you were able to: - successfully used model predictive control to manipulate the postion of a quadcopter -- Show graphical relationships between significant varables. Next Steps: - Try adjusting the initial conditions to see how the behavior of the quadcopter changes! + + From 094795619a1c0fc2abd9ae552f9231085e3786d8 Mon Sep 17 00:00:00 2001 From: rishimandyam Date: Mon, 8 Jul 2024 13:39:13 -0500 Subject: [PATCH 04/12] added images --- docs/src/assets/Quadcopter_Obj_NN.png | Bin 0 -> 28917 bytes docs/src/assets/Quadcopter_Xpos.png | Bin 0 -> 27743 bytes docs/src/assets/Quadcopter_Ypos.png | Bin 0 -> 27436 bytes docs/src/assets/Quadcopter_Zpos.png | Bin 0 -> 29353 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/src/assets/Quadcopter_Obj_NN.png create mode 100644 docs/src/assets/Quadcopter_Xpos.png create mode 100644 docs/src/assets/Quadcopter_Ypos.png create mode 100644 docs/src/assets/Quadcopter_Zpos.png diff --git a/docs/src/assets/Quadcopter_Obj_NN.png b/docs/src/assets/Quadcopter_Obj_NN.png new file mode 100644 index 0000000000000000000000000000000000000000..2e3a23831db438fa9324df3a25ed1a90432a2061 GIT binary patch literal 28917 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 roNlv8G|948yp literal 0 HcmV?d00001 diff --git a/docs/src/assets/Quadcopter_Xpos.png b/docs/src/assets/Quadcopter_Xpos.png new file mode 100644 index 0000000000000000000000000000000000000000..8196e328a2b7dc375702b72d8463ec0cedd30d7d GIT binary patch literal 27743 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 literal 0 HcmV?d00001 diff --git a/docs/src/assets/Quadcopter_Zpos.png b/docs/src/assets/Quadcopter_Zpos.png new file mode 100644 index 0000000000000000000000000000000000000000..1ec371dbde3b765e6813377ca603957696b36c07 GIT binary patch literal 29353 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 Date: Wed, 10 Jul 2024 15:09:59 -0500 Subject: [PATCH 05/12] added edits to documentation --- docs/src/tutorials/quadcopter.md | 306 +++++++++++++------------------ 1 file changed, 131 insertions(+), 175 deletions(-) diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index 12fb6402..6e88d808 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -3,7 +3,7 @@ By: Rishi Mandyam This tutorial notebook is an introduction to the graph-based modeling framework -Plasmo.jl (PLatform for Scalable Modeling and Optimization) for JuMP +Plasmo.jl (Platform for Scalable Modeling and Optimization) for JuMP (Julia for Mathematical Programming). The following problem comes from the paper of Na, Shin, Anitescu, and Zavala (available [here](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9840913)). @@ -11,9 +11,25 @@ The following problem comes from the paper of Na, Shin, Anitescu, and Zavala (a A quadcopter operates in 3-D space with positions $(x, y, z)$ and angles ($\gamma$, $\beta$, and $\alpha$). $g$ is the graviational constant. The set of state variables at time $t$ are treated as $\boldsymbol{x}_t = (x, y, z, \dot{x}, \dot{y}, \dot{z}, \gamma, \beta, \alpha)$. The input variables at time $t$ are $\boldsymbol{u}_t = (a, \omega_x, \omega_y, \omega_z)$. -The quadcopter operates according to the constraints -### 1. To begin we will import and use the necessary packages +The quadcopter control problem can be written as an optimization problem as: + +$$ +\begin{align*} + \min &\; \phi(t) := \int_{0}^T \frac{1}{2} (\boldsymbol{x}(t) - \boldsymbol{x}(t)^{ref})^\top Q (\boldsymbol{x}(t) - \boldsymbol{x}(t)^{ref}) + \boldsymbol{u}(t)^\top R \boldsymbol{u}(t) dt \\ + \textrm{s.t.} &\; \frac{d^2x}{dt^2} = a (\cos\gamma \sin \beta \cos \alpha + \sin \gamma \sin \alpha) \\ +&\; \frac{d^2 y}{dt^2} = a (\cos \gamma \sin \beta \sin \alpha - \sin \gamma \cos \alpha) \\ +&\; \frac{d^2 z}{dt^2} = a \cos \gamma \cos \beta - g \\ +&\; \frac{d\gamma}{dt} = (\omega_x \cos \gamma + \omega_y \sin \gamma) / \cos \beta\\ +&\;\frac{d\beta}{dt} = -\omega_x \sin \gamma + \omega_y \cos \gamma \\ +&\;\frac{d\alpha}{dt} = \omega_x \cos \gamma \tan \beta + \omega_y \sin \gamma \tan \beta + \omega_z +\end{align*} +$$ + +We will model this problem in Plasmo by discretizing the problem into finite time points and representing each time point with a node. + +### 1. Import Packages +To begin, we will import and use the necessary packages ```julia using JuMP @@ -23,193 +39,142 @@ using Plots using LinearAlgebra ``` -### 2. Lets Design our function -This function will: +### 2. Function Design + +We will define a function called `Quad` that will take arguments for the number of nodes and the discretization size (i.e., $\Delta t$), optimize the model, and return the graph and reference values $x^{ref}$. The function inputs are: -- number of nodes (N) -- time discretization (number of seconds between nodes [dt]) +- number of nodes (`N`) +- time discretization (number of seconds between nodes `dt`) The function outputs are: -- The objective value of the solved graph which is: - - The sum of all variables in all nodes of the graph -- an array containing each node in the graph (nodes) -- an array with the reference values on each node (xk_ref) +- The graph +- The objective value of the discretized form of $\phi$ +- An array with the reference values on each node ($x^{ref}$) +We first establish the set points for each timepoint. The quadcopter will fly in a linear upward path in the positive X, Y, and Z directions. ```julia - function Quad(N, dt) -``` - - Establish the setpoints for each timepoint - In this example the quadcopter will fly in a linear upward path - in the positive X, Y, and Z directions -```julia =# - X_ref = 0:10/N:10; - dXdt_ref = 0:10/N:10; - Y_ref = 0:10/N:10; - dYdt_ref = 0:10/N:10; - Z_ref = 0:10/N:10; - dZdt_ref = 0:10/N:10; - g_ref = 0:10/N:10; - b_ref = 0:10/N:10; - a_ref = 0:10/N:10; +function Quad(N, dt) + X_ref = 0:10/N:10; + dXdt_ref = zeros(N); + Y_ref = 0:10/N:10; + dYdt_ref = zeros(N) + Z_ref = 0:10/N:10; + dZdt_ref = zeros(N); + g_ref = zeros(N); + b_ref = zeros(N); + a_ref = zeros(N); ``` -Define the vector of setpoints -$\boldsymbol{x}_t = (x, y, z, \dot{x}, \dot{y}, \dot{z}, \gamma, \beta, \alpha)$ +We next combine these vectors into another vector which we call $x^{ref}$, and then define the necessary constants and arrays. ```julia - xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref]; -``` -Define Constants and arrays -```julia - grav = 9.8 # m/s^2 + xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref]; + grav = 9.8 # m/s^2 - Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); - R = diagm([1/10, 1/10, 1/10, 1/10]); -``` - -Transpose Reference Matrix -```Julia - xk_ref1 = zeros(N,9) - for i in (1:N) - for j in 1:length(xk_ref) - xk_ref1[i,j] = xk_ref[j][i] - end - end - - uk = [C_a, wx, wy, wz] -``` + Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); + R = diagm([1/10, 1/10, 1/10, 1/10]); -Initialize the model optimizer -```Julia - QuadCopter = Model(Ipopt.Optimizer) + xk_ref1 = xk_ref' ``` -Create the optigraph +We can now begin building the OptiGraph. We now initialize an OptiGraph and set the optimizer. We then define `N` nodes on the OptiGraph `graph`. ```Julia - graph = OptiGraph() + graph = OptiGraph() + solver = optimizer_with_attributes(Ipopt.Optimizer, "max_iter" => 100) + set_optimizer(graph, solver) + @optinode(graph, nodes[1:N]) ``` -Set the maximum amount of iterations for the optimizer. -```julia - set_optimizer_attribute(graph, "max_iter", 100) -``` -Create N optinodes on the graph -```julia - @optinode(graph, nodes[1:N]) -``` - -Add the function variables and constraints to each node in the graph +Next, we loop through the nodes, and define variables on each node. These variables include dummy variables for the second derivatives and the angles' derivatives to avoid nonlinearities in the linking constraints. We also define the objective function and define arrays for the variables (to simplify the objective function formulation). ```julia - for (i, node) in enumerate(nodes) - - # Create Variables in each of the nodes - @variable(node, g) - @variable(node, b) - @variable(node, a) + for (i, node) in enumerate(nodes) - @variable(node, X) - @variable(node, Y) - @variable(node, Z) + # Create state variables + @variable(node, g) + @variable(node, b) + @variable(node, a) - @variable(node, dXdt) - @variable(node, dYdt) - @variable(node, dZdt) + @variable(node, X) + @variable(node, Y) + @variable(node, Z) - @variable(node, C_a) - - if i == 1 # Set the initial value of each variable to 0 - @constraint(node, X == 0) - @constraint(node, Y == 0) - @constraint(node, Z == 0) - @constraint(node, dXdt == 0) - @constraint(node, dYdt == 0) - @constraint(node, dZdt == 0) - @constraint(node, g == 0) - @constraint(node, b == 0) - @constraint(node, a == 0) - end -``` + @variable(node, dXdt) + @variable(node, dYdt) + @variable(node, dZdt) -Establish the variables, nonlinear constraints, and objective functions given in the problem statement + # Create input variables + + @variable(node, C_a) + @variable(node, wx) + @variable(node, wy) + @variable(node, wz) -\begin{align*} -\frac{d^2x}{dt^2} &= a (\cos\gamma \sin \beta \cos \alpha + \sin \gamma \sin \alpha) \\ -\frac{d^2 y}{dt^2} &= a (\cos \gamma \sin \beta \sin \alpha - \sin \gamma \cos \alpha) \\ -\frac{d^2 z}{dt^2} &= a \cos \gamma \cos \beta - g \\ -\frac{d\gamma}{dt} &= (\omega_x \cos \gamma + \omega_y \sin \gamma) / \cos \beta\\ -\frac{d\beta}{dt} &= -\omega_x \sin \gamma + \omega_y \cos \gamma \\ -\frac{d\alpha}{dt} &= \omega_x \cos \gamma \tan \beta + \omega_y \sin \gamma \tan \beta + \omega_z -\end{align*} + # Define variables for derivatives (these are dummy variables, which are not necessary on v0.6+) + + @variable(node, d2Xdt2) + @variable(node, d2Ydt2) + @variable(node, d2Zdt2) + @variable(node, dgdt) + @variable(node, dbdt) + @variable(node, dadt) + + # These variables are dummy variables to create linear linking constraints on the edges. + @NLconstraint(node, d2Xdt2 == C_a*(cos(g)*sin(b)*cos(a) + sin(g)*sin(a))) + @NLconstraint(node, d2Ydt2 == C_a*(cos(g)*sin(b)*sin(a) + sin(g)*cos(a))) + @NLconstraint(node, d2Zdt2 == C_a*cos(g)*cos(b) - grav) + + @NLconstraint(node, dgdt == (wx*cos(g) + wy*sin(g))/(cos(b))) + @NLconstraint(node, dbdt == -wx*sin(g) + wy*cos(g)) + @NLconstraint(node, dadt == wx*cos(g)*tan(b) + wy*sin(g)*tan(b) + wz) -The input variables at time $t$ are $\boldsymbol{u}_t = (a, \omega_x, \omega_y, \omega_z)$ + xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables + xk1 = xk-xk_ref1[i,:] # Array to hold the difference between variable values and their setpoints. + + uk = [C_a, wx, wy, wz] + @objective(node, Min, (1/2*(xk1')*Q*(xk1) + 1/2*(uk')*R*(uk)) * dt) + end +``` +We also set initial conditions on the first variables (for the differential equations). Note that the variables can be referenced by calling `nodes[1][]` ```julia - @variable(node, d2Xdt2) - @NLconstraint(node, d2Xdt2 == C_a*(cos(g)*sin(b)*cos(a) + sin(g)*sin(a))) - @variable(node, d2Ydt2) - @NLconstraint(node, d2Ydt2 == C_a*(cos(g)*sin(b)*sin(a) + sin(g)*cos(a))) - @variable(node, d2Zdt2) - @NLconstraint(node, d2Zdt2 == C_a*cos(g)*cos(b) - grav) - - @variable(node, wx) - @variable(node, wy) - @variable(node, wz) - - @variable(node, dgdt) - @NLconstraint(node, dgdt == (wx*cos(g) + wy*sin(g))/(cos(b))) - @variable(node, dbdt) - @NLconstraint(node, dbdt == -wx*sin(g) + wy*cos(g)) - @variable(node, dadt) - @NLconstraint(node, dadt == wx*cos(g)*tan(b) + wy*sin(g)*tan(b) + wz) + @constraint(nodes[1], nodes[1][:X] == 0) + @constraint(nodes[1], nodes[1][:Y] == 0) + @constraint(nodes[1], nodes[1][:Z] == 0) + @constraint(nodes[1], nodes[1][:dXdt] == 0) + @constraint(nodes[1], nodes[1][:dYdt] == 0) + @constraint(nodes[1], nodes[1][:dZdt] == 0) + @constraint(nodes[1], nodes[1][:g] == 0) + @constraint(nodes[1], nodes[1][:b] == 0) + @constraint(nodes[1], nodes[1][:a] == 0) ``` -```julia - xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables - xk1 = xk-xk_ref1[i,:] # Array to hold the difference between variable values and their setpoints. - - uk = [C_a, wx, wy, wz] -``` -Establish the objective function. This is the same as the stage cost function given in problem statement. - -$\phi := \frac{1}{2} (\boldsymbol{x}_t - \boldsymbol{x}^{ref}_t)^\top Q (\boldsymbol{x} - \boldsymbol{x}^{ref}_t) + \boldsymbol{u}^\top R \boldsymbol{u}$ -where -$\boldsymbol{x}^{ref}_t$ are the reference values at time $t$. -```julia +With the variables defined, we can now add linking constraints between each time point. Note that variables like `d2Xdt2` are the right hand side of the constraints for the derivative of `dXdt`. As this is nonlinear, we use this dummy variable in the linking constraint. - @objective(node, Min, (1/2*(xk1')*Q*(xk1) + 1/2*(uk')*R*(uk)) * dt) #row Q column - - end -``` -Add link constraints between nodes using the explicit Euler's scheme. ```julia - for i in 1:(N-1) # iterate through each node except the last - - @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) - @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) - @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) - - @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) - @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) - @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) + for i in 1:(N-1) # iterate through each node except the last + @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) + @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) + @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) - @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) - @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) - @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) + @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) + @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) + @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) - end + @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) + @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) + @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) + end ``` -Set and call the optimizer +We now have all the constraints and variables defined and can call `optimize!` on the graph. + ```julia + optimize!(graph); - set_optimizer(graph, Ipopt.Optimizer); - set_optimizer_attribute(graph, "max_iter", 700); - - optimize!(graph); - end + return objective_value(graph), graph, xk_ref +end ``` Now that we have created our function to model the behavior of the quadcopter, we can test it using some example cases. @@ -218,10 +183,10 @@ we can test it using some example cases. - N = 50 time points - dt = 0.1 seconds ```julia -N = 100 -dt = .1 -objv, nodes, xk_ref = Quad(N, dt) - +N = 50 +dt = 0.1 +objv, graph, xk_ref = Quad(N, dt) +nodes = getnodes(graph) # create empty arrays CAval_array = zeros(length(nodes)) xval_array = zeros(length(nodes)) @@ -248,24 +213,21 @@ zarray = Array{Array}(undef, 2) zarray[1] = zval_array zarray[2] = 0:10/(N-1):10 ``` -### Let's see the position of the quadcopter in relation to its setpoint in all three dimentions - +Now, let's visualize the position of the quadcopter in relation to its setpoint in each dimension. Below is the code for doing so in the x-dimension, and the code can be adapted for the y and z dimensions. ```julia plot((1:length(xval_array)), xarray[1:end], title = "X value over time", xlabel = "Node (N)", ylabel = "X Value", - label = ["Current X position" "X Setpoint"]) + label = ["Current X position" "X Setpoint"] +) ``` -Repeat This process for the Y-position and Z-position - -### Your plots should look something like this drawing drawing drawing -### Now that we have solved for the optimal solution, lets explore some other correlations. +Now that we have solved for the optimal solution, let's explore some other correlations. Let's see how increasing the number of nodes changes the objective value of the system ```julia @@ -278,7 +240,7 @@ obj_val_N = zeros(N) for i in 1:length(time_steps) timing = @elapsed begin - objval, nodes, xk_ref = Quad(time_steps[i], 10 / time_steps[i]); + objval, graph, xk_ref = Quad(time_steps[i], 10 / time_steps[i]); obj_val_N[i] = objval end println("Done with iteration $i after ", timing, " seconds") @@ -292,6 +254,7 @@ Quad_Obj_NN = plot(time_steps, obj_val_N, ``` drawing + The plot shows that as you increase the number of nodes, the objective value of the system decreases. Let's see how changing the dt value changes the objective value of the system. @@ -302,7 +265,7 @@ dt_array = .5:.5:5 obj_val_dt = zeros(length(dt_array)) for (i, dt_val) in enumerate(dt_array) - objval, nodes = Quad(N, dt_val); + objval, graph, xk_ref = Quad(N, dt_val); obj_val_dt[i] = objval end @@ -313,15 +276,8 @@ plot((1:length(obj_val_dt))*dt, obj_val_dt, xlabel = "dt value", ylabel = "Objective Value", legend = :none, color = :black, - linewidth = 2) + linewidth = 2 +) ``` -### Conclusion -In this tutorial you were able to: -- successfully used model predictive control to manipulate the postion of a quadcopter - -Next Steps: -- Try adjusting the initial conditions to see how the behavior of the quadcopter changes! - - From 23135801073428c30bc1ccf5057d2949b43d3d9b Mon Sep 17 00:00:00 2001 From: dlcole3 Date: Wed, 14 Aug 2024 14:11:45 -0500 Subject: [PATCH 06/12] correction after merge, added code example --- docs/make.jl | 5 +- docs/src/tutorials/quadcopter.md | 2 +- examples/quadcopter_example.jl | 194 +++++++++++++++++++++++++++++++ 3 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 examples/quadcopter_example.jl diff --git a/docs/make.jl b/docs/make.jl index 881e0683..228ad570 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -23,10 +23,7 @@ makedocs(; "Introduction" => "index.md", "Quickstart" => "documentation/quickstart.md", "Modeling with OptiGraphs" => "documentation/modeling.md", - "Graph Partitioning and Processing" => "documentation/partitioning.md", - "Tutorials" => - ["Optimal Control of a Natural Gas Network" => "tutorials/gas_pipeline.md", - "Optimal Control of a Quadcopter" => "tutorials/quadcopter.md"], + "Graph Processing and Analysis" => "documentation/graph_processing.md", "API Documentation" => "documentation/api_docs.md", "Tutorials" => [ "Supply Chain Optimization" => "tutorials/supply_chain.md", diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index 1e410b70..1e419b78 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -4,7 +4,7 @@ By: Rishi Mandyam This tutorial notebook is an introduction to the graph-based modeling framework Plasmo.jl (Platform for Scalable Modeling and Optimization) for JuMP -(Julia for Mathematical Programming). +(Julia for Mathematical Programming). The code below as a single Julia file can be found [here](https://github.com/plasmo-dev/Plasmo.jl/tree/main/examples/quadcopter_example.jl). The following problem comes from the paper of Na, Shin, Anitescu, and Zavala (available [here](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9840913)). diff --git a/examples/quadcopter_example.jl b/examples/quadcopter_example.jl new file mode 100644 index 00000000..0081e740 --- /dev/null +++ b/examples/quadcopter_example.jl @@ -0,0 +1,194 @@ +# This script requires having v0.6.0+ of Plasmo.jl + +using JuMP +using Plasmo +using Ipopt +using Plots +using LinearAlgebra + +function Quad(N, dt) + X_ref = 0:10/N:10; + dXdt_ref = zeros(N); + Y_ref = 0:10/N:10; + dYdt_ref = zeros(N) + Z_ref = 0:10/N:10; + dZdt_ref = zeros(N); + g_ref = zeros(N); + b_ref = zeros(N); + a_ref = zeros(N); + + xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref]; + grav = 9.8 # m/s^2 + + Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); + R = diagm([1/10, 1/10, 1/10, 1/10]); + + xk_ref1 = zeros(N,9) + for i in (1:N) + for j in 1:length(xk_ref) + xk_ref1[i,j] = xk_ref[j][i] + end + end + + graph = OptiGraph() + solver = optimizer_with_attributes(Ipopt.Optimizer, "max_iter" => 100) + set_optimizer(graph, solver) + @optinode(graph, nodes[1:N]) + for (i, node) in enumerate(nodes) + + # Create state variables + @variable(node, g) + @variable(node, b) + @variable(node, a) + + @variable(node, X) + @variable(node, Y) + @variable(node, Z) + + @variable(node, dXdt) + @variable(node, dYdt) + @variable(node, dZdt) + + # Create input variables + @variable(node, C_a) + @variable(node, wx) + @variable(node, wy) + @variable(node, wz) + + # These expressions to simplify the linking constraints later + @expression(node, d2Xdt2, C_a * (cos(g) * sin(b) * cos(a) + sin(g) * sin(a))) + @expression(node, d2Ydt2, C_a * (cos(g) * sin(b) * sin(a) + sin(g) * cos(a))) + @expression(node, d2Zdt2, C_a * cos(g) * cos(b) - grav) + + @expression(node, dgdt, (wx * cos(g) + wy * sin(g)) / (cos(b))) + @expression(node, dbdt, - wx * sin(g) + wy * cos(g)) + @expression(node, dadt, wx * cos(g) * tan(b) + wy * sin(g) * tan(b) + wz) + + xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables + + xk1 = xk .- xk_ref1[i, :] # Array to hold the difference between variable values and their setpoints. + + uk = [C_a, wx, wy, wz] + @objective(node, Min, (1 / 2 * (xk1') * Q * (xk1) + 1 / 2 * (uk') * R * (uk)) * dt) + end + + @constraint(nodes[1], nodes[1][:X] == 0) + @constraint(nodes[1], nodes[1][:Y] == 0) + @constraint(nodes[1], nodes[1][:Z] == 0) + @constraint(nodes[1], nodes[1][:dXdt] == 0) + @constraint(nodes[1], nodes[1][:dYdt] == 0) + @constraint(nodes[1], nodes[1][:dZdt] == 0) + @constraint(nodes[1], nodes[1][:g] == 0) + @constraint(nodes[1], nodes[1][:b] == 0) + @constraint(nodes[1], nodes[1][:a] == 0) + + for i in 1:(N-1) # iterate through each node except the last + @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) + @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) + @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) + + @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) + @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) + @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) + + @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) + @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) + @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) + end + + set_to_node_objectives(graph) + + optimize!(graph); + + return objective_value(graph), graph, xk_ref +end + + +N = 50 +dt = 0.1 +objv, graph, xk_ref = Quad(N, dt) +nodes = get_nodes(graph) +# create empty arrays +CAval_array = zeros(length(nodes)) +xval_array = zeros(length(nodes)) +yval_array = zeros(length(nodes)) +zval_array = zeros(length(nodes)) + +# add values to arrays +for (i, node) in enumerate(nodes) + CAval_array[i] = value(node[:C_a]) + xval_array[i] = value(node[:X]) + yval_array[i] = value(node[:Y]) + zval_array[i] = value(node[:Z]) +end + +xarray = Array{Array}(undef, 2) +xarray[1] = xval_array +xarray[2] = 0:10/(N-1):10 + +yarray = Array{Array}(undef, 2) +yarray[1] = yval_array +yarray[2] = 0:10/(N-1):10 + +zarray = Array{Array}(undef, 2) +zarray[1] = zval_array +zarray[2] = 0:10/(N-1):10 + +plot((1:length(xval_array)), xarray[1:end], + title = "X value over time", + xlabel = "Node", + ylabel = "X Value", + label = ["Current X position" "X Setpoint"] +) + +plot((1:length(yval_array)), yarray[1:end], + title = "Y value over time", + xlabel = "Node", + ylabel = "Y Value", + label = ["Current X position" "X Setpoint"] +) + +plot((1:length(zval_array)), zarray[1:end], + title = "Z value over time", + xlabel = "Node", + ylabel = "Z Value", + label = ["Current Z position" "Z Setpoint"] +) + +time_steps = 2:4:50 + +N = length(time_steps) +dt = .5 +obj_val_N = zeros(N) + + +for i in 1:length(time_steps) + timing = @elapsed begin + objval, graph, xk_ref = Quad(time_steps[i], 10 / time_steps[i]); + obj_val_N[i] = objval + end + println("Done with iteration $i after ", timing, " seconds") +end + +Quad_Obj_NN = plot(time_steps, obj_val_N, +title = "Objective Value vs Number of Nodes (N)", +xlabel = "Number of Nodes (N)", +ylabel = "Objective Value", +label = "Objective Value") + + +dt_array = .5:.5:5 +obj_val_dt = zeros(length(dt_array)) + +for (i, dt_val) in enumerate(dt_array) + objval, graph, xk_ref = Quad(N, dt_val); + obj_val_dt[i] = objval +end + +plot(dt_array, obj_val_dt, + title = "Objective Value vs dt", + xlabel = "dt value", + ylabel = "Objective Value", + legend = :none, color = :black, + linewidth = 2 +) From 755ce4b958713b80ccb43f27946fe8bcbb9ebb1c Mon Sep 17 00:00:00 2001 From: dlcole3 Date: Wed, 14 Aug 2024 14:34:42 -0500 Subject: [PATCH 07/12] Fixed math error --- docs/src/tutorials/quadcopter.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index 1e419b78..21ed7350 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -14,17 +14,17 @@ The input variables at time $t$ are $\boldsymbol{u}_t = (a, \omega_x, \omega_y, The quadcopter control problem can be written as an optimization problem as: -$$ +```math \begin{align*} \min &\; \phi(t) := \int_{0}^T \frac{1}{2} (\boldsymbol{x}(t) - \boldsymbol{x}(t)^{ref})^\top Q (\boldsymbol{x}(t) - \boldsymbol{x}(t)^{ref}) + \boldsymbol{u}(t)^\top R \boldsymbol{u}(t) dt \\ - \textrm{s.t.} &\; \frac{d^2x}{dt^2} = a (\cos\gamma \sin \beta \cos \alpha + \sin \gamma \sin \alpha) \\ -&\; \frac{d^2 y}{dt^2} = a (\cos \gamma \sin \beta \sin \alpha - \sin \gamma \cos \alpha) \\ -&\; \frac{d^2 z}{dt^2} = a \cos \gamma \cos \beta - g \\ -&\; \frac{d\gamma}{dt} = (\omega_x \cos \gamma + \omega_y \sin \gamma) / \cos \beta\\ -&\;\frac{d\beta}{dt} = -\omega_x \sin \gamma + \omega_y \cos \gamma \\ -&\;\frac{d\alpha}{dt} = \omega_x \cos \gamma \tan \beta + \omega_y \sin \gamma \tan \beta + \omega_z + \textrm{s.t.} &\; \frac{d^2x}{dt^2} = a (\cos(\gamma) \sin( \beta) \cos (\alpha) + \sin (\gamma) \sin (\alpha)) \\ +&\; \frac{d^2 y}{dt^2} = a (\cos (\gamma) \sin (\beta) \sin (\alpha) - \sin (\gamma) \cos (\alpha)) \\ +&\; \frac{d^2 z}{dt^2} = a \cos (\gamma) \cos (\beta) - g \\ +&\; \frac{d\gamma}{dt} = (\omega_x \cos (\gamma) + \omega_y \sin (\gamma)) / \cos (\beta)\\ +&\;\frac{d\beta}{dt} = -\omega_x \sin (\gamma) + \omega_y \cos (\gamma) \\ +&\;\frac{d\alpha}{dt} = \omega_x \cos (\gamma) \tan (\beta) + \omega_y \sin (\gamma) \tan (\beta) + \omega_z \end{align*} -$$ +``` We will model this problem in Plasmo by discretizing the problem into finite time points and representing each time point with a node. From 3324460f62442d8d92fe80cf8cdedd30104f16b3 Mon Sep 17 00:00:00 2001 From: dlcole3 Date: Wed, 21 Aug 2024 16:56:20 -0500 Subject: [PATCH 08/12] removed the example from the examples folder --- docs/src/tutorials/quadcopter.md | 2 +- examples/quadcopter_example.jl | 194 ------------------------------- 2 files changed, 1 insertion(+), 195 deletions(-) delete mode 100644 examples/quadcopter_example.jl diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index 21ed7350..ea164a34 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -4,7 +4,7 @@ By: Rishi Mandyam This tutorial notebook is an introduction to the graph-based modeling framework Plasmo.jl (Platform for Scalable Modeling and Optimization) for JuMP -(Julia for Mathematical Programming). The code below as a single Julia file can be found [here](https://github.com/plasmo-dev/Plasmo.jl/tree/main/examples/quadcopter_example.jl). +(Julia for Mathematical Programming). The following problem comes from the paper of Na, Shin, Anitescu, and Zavala (available [here](https://ieeexplore.ieee.org/stamp/stamp.jsp?arnumber=9840913)). diff --git a/examples/quadcopter_example.jl b/examples/quadcopter_example.jl deleted file mode 100644 index 0081e740..00000000 --- a/examples/quadcopter_example.jl +++ /dev/null @@ -1,194 +0,0 @@ -# This script requires having v0.6.0+ of Plasmo.jl - -using JuMP -using Plasmo -using Ipopt -using Plots -using LinearAlgebra - -function Quad(N, dt) - X_ref = 0:10/N:10; - dXdt_ref = zeros(N); - Y_ref = 0:10/N:10; - dYdt_ref = zeros(N) - Z_ref = 0:10/N:10; - dZdt_ref = zeros(N); - g_ref = zeros(N); - b_ref = zeros(N); - a_ref = zeros(N); - - xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref]; - grav = 9.8 # m/s^2 - - Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); - R = diagm([1/10, 1/10, 1/10, 1/10]); - - xk_ref1 = zeros(N,9) - for i in (1:N) - for j in 1:length(xk_ref) - xk_ref1[i,j] = xk_ref[j][i] - end - end - - graph = OptiGraph() - solver = optimizer_with_attributes(Ipopt.Optimizer, "max_iter" => 100) - set_optimizer(graph, solver) - @optinode(graph, nodes[1:N]) - for (i, node) in enumerate(nodes) - - # Create state variables - @variable(node, g) - @variable(node, b) - @variable(node, a) - - @variable(node, X) - @variable(node, Y) - @variable(node, Z) - - @variable(node, dXdt) - @variable(node, dYdt) - @variable(node, dZdt) - - # Create input variables - @variable(node, C_a) - @variable(node, wx) - @variable(node, wy) - @variable(node, wz) - - # These expressions to simplify the linking constraints later - @expression(node, d2Xdt2, C_a * (cos(g) * sin(b) * cos(a) + sin(g) * sin(a))) - @expression(node, d2Ydt2, C_a * (cos(g) * sin(b) * sin(a) + sin(g) * cos(a))) - @expression(node, d2Zdt2, C_a * cos(g) * cos(b) - grav) - - @expression(node, dgdt, (wx * cos(g) + wy * sin(g)) / (cos(b))) - @expression(node, dbdt, - wx * sin(g) + wy * cos(g)) - @expression(node, dadt, wx * cos(g) * tan(b) + wy * sin(g) * tan(b) + wz) - - xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables - - xk1 = xk .- xk_ref1[i, :] # Array to hold the difference between variable values and their setpoints. - - uk = [C_a, wx, wy, wz] - @objective(node, Min, (1 / 2 * (xk1') * Q * (xk1) + 1 / 2 * (uk') * R * (uk)) * dt) - end - - @constraint(nodes[1], nodes[1][:X] == 0) - @constraint(nodes[1], nodes[1][:Y] == 0) - @constraint(nodes[1], nodes[1][:Z] == 0) - @constraint(nodes[1], nodes[1][:dXdt] == 0) - @constraint(nodes[1], nodes[1][:dYdt] == 0) - @constraint(nodes[1], nodes[1][:dZdt] == 0) - @constraint(nodes[1], nodes[1][:g] == 0) - @constraint(nodes[1], nodes[1][:b] == 0) - @constraint(nodes[1], nodes[1][:a] == 0) - - for i in 1:(N-1) # iterate through each node except the last - @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) - @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) - @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) - - @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) - @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) - @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) - - @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) - @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) - @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) - end - - set_to_node_objectives(graph) - - optimize!(graph); - - return objective_value(graph), graph, xk_ref -end - - -N = 50 -dt = 0.1 -objv, graph, xk_ref = Quad(N, dt) -nodes = get_nodes(graph) -# create empty arrays -CAval_array = zeros(length(nodes)) -xval_array = zeros(length(nodes)) -yval_array = zeros(length(nodes)) -zval_array = zeros(length(nodes)) - -# add values to arrays -for (i, node) in enumerate(nodes) - CAval_array[i] = value(node[:C_a]) - xval_array[i] = value(node[:X]) - yval_array[i] = value(node[:Y]) - zval_array[i] = value(node[:Z]) -end - -xarray = Array{Array}(undef, 2) -xarray[1] = xval_array -xarray[2] = 0:10/(N-1):10 - -yarray = Array{Array}(undef, 2) -yarray[1] = yval_array -yarray[2] = 0:10/(N-1):10 - -zarray = Array{Array}(undef, 2) -zarray[1] = zval_array -zarray[2] = 0:10/(N-1):10 - -plot((1:length(xval_array)), xarray[1:end], - title = "X value over time", - xlabel = "Node", - ylabel = "X Value", - label = ["Current X position" "X Setpoint"] -) - -plot((1:length(yval_array)), yarray[1:end], - title = "Y value over time", - xlabel = "Node", - ylabel = "Y Value", - label = ["Current X position" "X Setpoint"] -) - -plot((1:length(zval_array)), zarray[1:end], - title = "Z value over time", - xlabel = "Node", - ylabel = "Z Value", - label = ["Current Z position" "Z Setpoint"] -) - -time_steps = 2:4:50 - -N = length(time_steps) -dt = .5 -obj_val_N = zeros(N) - - -for i in 1:length(time_steps) - timing = @elapsed begin - objval, graph, xk_ref = Quad(time_steps[i], 10 / time_steps[i]); - obj_val_N[i] = objval - end - println("Done with iteration $i after ", timing, " seconds") -end - -Quad_Obj_NN = plot(time_steps, obj_val_N, -title = "Objective Value vs Number of Nodes (N)", -xlabel = "Number of Nodes (N)", -ylabel = "Objective Value", -label = "Objective Value") - - -dt_array = .5:.5:5 -obj_val_dt = zeros(length(dt_array)) - -for (i, dt_val) in enumerate(dt_array) - objval, graph, xk_ref = Quad(N, dt_val); - obj_val_dt[i] = objval -end - -plot(dt_array, obj_val_dt, - title = "Objective Value vs dt", - xlabel = "dt value", - ylabel = "Objective Value", - legend = :none, color = :black, - linewidth = 2 -) From 899ba1c46d47a7a6c1276501f1365b8a5b5be5ad Mon Sep 17 00:00:00 2001 From: dlcole3 Date: Thu, 22 Aug 2024 15:02:58 -0500 Subject: [PATCH 09/12] Changed function format --- docs/src/tutorials/quadcopter.md | 138 +++++++++++++++++-------------- 1 file changed, 78 insertions(+), 60 deletions(-) diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index ea164a34..375fd79d 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -42,7 +42,7 @@ using LinearAlgebra ### 2. Function Design -We will define a function called `Quad` that will take arguments for the number of nodes and the discretization size (i.e., $\Delta t$), optimize the model, and return the graph and reference values $x^{ref}$. +We will define a function called `build_quadcopter_graph` that will take arguments for the number of nodes and the discretization size (i.e., $\Delta t$), optimize the model, and return the graph and reference values $x^{ref}$. The function inputs are: - number of nodes (`N`) @@ -53,64 +53,28 @@ The function outputs are: - The objective value of the discretized form of $\phi$ - An array with the reference values on each node ($x^{ref}$) -We first establish the set points for each timepoint. The quadcopter will fly in a linear upward path in the positive X, Y, and Z directions. -```julia -function Quad(N, dt) - X_ref = 0:10/N:10; - dXdt_ref = zeros(N); - Y_ref = 0:10/N:10; - dYdt_ref = zeros(N) - Z_ref = 0:10/N:10; - dZdt_ref = zeros(N); - g_ref = zeros(N); - b_ref = zeros(N); - a_ref = zeros(N); -``` - -We next combine these vectors into another vector which we call $x^{ref}$, and then define the necessary constants and arrays. +The `build_quadcopter_graph` function will use three supporting functions that will add variables, add constraints (both local and linking) and add the objectives to the nodes. These functions will be detailed below before they are used to build the full quadcopter graph. +The first function we call `add_variables!`, which defines each of the decision variables as well as some supporting expressions. Here, we loop through the nodes, and define variables on each node. These variables include expressions that will simplify forming the linking constrints (these are the right hand sides of the derivatives). ```julia - xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref]; +function add_variables!(nodes) grav = 9.8 # m/s^2 - - Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); - R = diagm([1/10, 1/10, 1/10, 1/10]); - xk_ref1 = zeros(N,9) - for i in (1:N) - for j in 1:length(xk_ref) - xk_ref1[i,j] = xk_ref[j][i] - end - end -``` - -We can now begin building the OptiGraph. We now initialize an OptiGraph and set the optimizer. We then define `N` nodes on the OptiGraph `graph`. -```Julia - graph = OptiGraph() - solver = optimizer_with_attributes(Ipopt.Optimizer, "max_iter" => 100) - set_optimizer(graph, solver) - @optinode(graph, nodes[1:N]) -``` - -Next, we loop through the nodes, and define variables on each node. These variables include dummy variables for the second derivatives and the angles' derivatives to avoid nonlinearities in the linking constraints. We also define the objective function and define arrays for the variables (to simplify the objective function formulation). -```julia for (i, node) in enumerate(nodes) - # Create state variables @variable(node, g) @variable(node, b) @variable(node, a) - + @variable(node, X) @variable(node, Y) @variable(node, Z) - + @variable(node, dXdt) @variable(node, dYdt) @variable(node, dZdt) # Create input variables - @variable(node, C_a) @variable(node, wx) @variable(node, wy) @@ -124,17 +88,16 @@ Next, we loop through the nodes, and define variables on each node. These variab @expression(node, dgdt, (wx * cos(g) + wy * sin(g)) / (cos(b))) @expression(node, dbdt, - wx * sin(g) + wy * cos(g)) @expression(node, dadt, wx * cos(g) * tan(b) + wy * sin(g) * tan(b) + wz) - - xk = [X, dXdt, Y, dYdt, Z, dZdt, g, b, a] # Array to hold variables - xk1 = xk .- xk_ref1[i,:] # Array to hold the difference between variable values and their setpoints. - - uk = [C_a, wx, wy, wz] - @objective(node, Min, (1 / 2 * (xk1') * Q * (xk1) + 1 / 2 * (uk') * R * (uk)) * dt) end -``` -We also set initial conditions on the first variables (for the differential equations). Note that the variables can be referenced by calling `nodes[1][]` +end +``` + +Next, we define a function for adding the constraints to the graph. We will set the initial values at time 0 and then define the linking constraints, which are discretized derivatives. Note that both linear and nonlinear constraints are handled in the same way by the user in both the `@constraint` and `@linkconstraint` macros. ```julia +function add_constraints!(graph, nodes, dt) + N = length(nodes) + @constraint(nodes[1], nodes[1][:X] == 0) @constraint(nodes[1], nodes[1][:Y] == 0) @constraint(nodes[1], nodes[1][:Z] == 0) @@ -144,36 +107,91 @@ We also set initial conditions on the first variables (for the differential equa @constraint(nodes[1], nodes[1][:g] == 0) @constraint(nodes[1], nodes[1][:b] == 0) @constraint(nodes[1], nodes[1][:a] == 0) -``` -With the variables defined, we can now add linking constraints between each time point. Note that expressions like `d2Xdt2` are the right hand side of the constraints for the derivative of `dXdt`. - -```julia for i in 1:(N-1) # iterate through each node except the last @linkconstraint(graph, nodes[i+1][:dXdt] == dt*nodes[i][:d2Xdt2] + nodes[i][:dXdt]) @linkconstraint(graph, nodes[i+1][:dYdt] == dt*nodes[i][:d2Ydt2] + nodes[i][:dYdt]) @linkconstraint(graph, nodes[i+1][:dZdt] == dt*nodes[i][:d2Zdt2] + nodes[i][:dZdt]) - + @linkconstraint(graph, nodes[i+1][:g] == dt*nodes[i][:dgdt] + nodes[i][:g]) @linkconstraint(graph, nodes[i+1][:b] == dt*nodes[i][:dbdt] + nodes[i][:b]) @linkconstraint(graph, nodes[i+1][:a] == dt*nodes[i][:dadt] + nodes[i][:a]) - + @linkconstraint(graph, nodes[i+1][:X] == dt*nodes[i][:dXdt] + nodes[i][:X]) @linkconstraint(graph, nodes[i+1][:Y] == dt*nodes[i][:dYdt] + nodes[i][:Y]) @linkconstraint(graph, nodes[i+1][:Z] == dt*nodes[i][:dZdt] + nodes[i][:Z]) end +end ``` -We now have all the constraints and variables defined and can call `optimize!` on the graph. +Next, we set a function for defining the objectives. The quadcopter will fly in a linear upward path in the positive X, Y, and Z directions. We combine these vectors into another vector which we call $x^{ref}$, and then define the necessary constants and arrays (this will simplify forming the objective function). ```julia +function add_objective!(nodes, N, dt) + X_ref = 0:10/N:10; + dXdt_ref = zeros(N); + Y_ref = 0:10/N:10; + dYdt_ref = zeros(N) + Z_ref = 0:10/N:10; + dZdt_ref = zeros(N); + g_ref = zeros(N); + b_ref = zeros(N); + a_ref = zeros(N); + + xk_ref = [X_ref, dXdt_ref, Y_ref, dYdt_ref, Z_ref, dZdt_ref, g_ref, b_ref, a_ref]; + + Q = diagm([1, 0, 1, 0, 1, 0, 1, 1, 1]); + R = diagm([1/10, 1/10, 1/10, 1/10]); + + xk_ref1 = zeros(N,9) + for i in (1:N) + for j in 1:length(xk_ref) + xk_ref1[i,j] = xk_ref[j][i] + end + end + + for (i, node) in enumerate(nodes) + xk = [ # Array to hold variables + node[:X], + node[:dXdt], + node[:Y], + node[:dYdt], + node[:Z], + node[:dZdt], + node[:g], + node[:b], + node[:a] + ] + + xk1 = xk .- xk_ref1[i, :] # Array to hold the difference between variable values and their setpoints. + + uk = [node[:C_a], node[:wx], node[:wy], node[:wz]] + @objective(node, Min, (1 / 2 * (xk1') * Q * (xk1) + 1 / 2 * (uk') * R * (uk)) * dt) + end + return xk_ref +end +``` + +We can now define the function for building the optigraph. We initialize an OptiGraph and set the optimizer. We then define `N` nodes on the OptiGraph `graph`. We then call the three functions above and call `set_to_node_objectives` to set the graph's objective to the nodes' objectives we have defined. We can then call `optimize` and return the objective value, the graph, and the reference points. +```Julia +function build_quadcopter_graph(N, dt) + graph = OptiGraph() + solver = optimizer_with_attributes(Ipopt.Optimizer, "max_iter" => 100) + set_optimizer(graph, solver) + @optinode(graph, nodes[1:N]) + + add_variables!(nodes) + add_constraints!(graph, nodes, dt) + xk_ref = add_objective!(nodes, N, dt) + set_to_node_objectives(graph) - + optimize!(graph); return objective_value(graph), graph, xk_ref end ``` + Now that we have created our function to model the behavior of the quadcopter, we can test it using some example cases. @@ -183,7 +201,7 @@ First, we will run an example with 50 time points (each represented by a node) w ```julia N = 50 dt = 0.1 -objv, graph, xk_ref = Quad(N, dt) +objv, graph, xk_ref = build_quadcopter_graph(N, dt) nodes = getnodes(graph) # create empty arrays CAval_array = zeros(length(nodes)) @@ -237,7 +255,7 @@ obj_val_N = zeros(N) for i in 1:length(time_steps) timing = @elapsed begin - objval, graph, xk_ref = Quad(time_steps[i], 10 / time_steps[i]); + objval, graph, xk_ref = build_quadcopter_graph(time_steps[i], 10 / time_steps[i]); obj_val_N[i] = objval end println("Done with iteration $i after ", timing, " seconds") From 49f4cac6cef00cd952b96388b8f544e79da82a1d Mon Sep 17 00:00:00 2001 From: dlcole3 Date: Thu, 22 Aug 2024 15:05:03 -0500 Subject: [PATCH 10/12] Changed order of bullets --- docs/src/tutorials/quadcopter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index 375fd79d..3cd7cf3b 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -49,8 +49,8 @@ The function inputs are: - time discretization (number of seconds between nodes `dt`) The function outputs are: -- The graph - The objective value of the discretized form of $\phi$ +- The graph - An array with the reference values on each node ($x^{ref}$) The `build_quadcopter_graph` function will use three supporting functions that will add variables, add constraints (both local and linking) and add the objectives to the nodes. These functions will be detailed below before they are used to build the full quadcopter graph. From 235c0d48c9e7261a462caefc039475e3d0de279a Mon Sep 17 00:00:00 2001 From: Jordan Jalving Date: Thu, 22 Aug 2024 16:52:07 -0700 Subject: [PATCH 11/12] fix typo --- docs/src/tutorials/quadcopter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index 3cd7cf3b..2b77f5c4 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -173,7 +173,7 @@ end ``` We can now define the function for building the optigraph. We initialize an OptiGraph and set the optimizer. We then define `N` nodes on the OptiGraph `graph`. We then call the three functions above and call `set_to_node_objectives` to set the graph's objective to the nodes' objectives we have defined. We can then call `optimize` and return the objective value, the graph, and the reference points. -```Julia +```julia function build_quadcopter_graph(N, dt) graph = OptiGraph() solver = optimizer_with_attributes(Ipopt.Optimizer, "max_iter" => 100) From 7abedbf84d94ecca00f073e89ce3feb57d43b724 Mon Sep 17 00:00:00 2001 From: Jordan Jalving Date: Thu, 22 Aug 2024 16:56:09 -0700 Subject: [PATCH 12/12] minor formatting --- docs/src/tutorials/quadcopter.md | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/docs/src/tutorials/quadcopter.md b/docs/src/tutorials/quadcopter.md index 2b77f5c4..f598bce0 100644 --- a/docs/src/tutorials/quadcopter.md +++ b/docs/src/tutorials/quadcopter.md @@ -39,7 +39,6 @@ using Plots using LinearAlgebra ``` - ### 2. Function Design We will define a function called `build_quadcopter_graph` that will take arguments for the number of nodes and the discretization size (i.e., $\Delta t$), optimize the model, and return the graph and reference values $x^{ref}$. @@ -164,7 +163,6 @@ function add_objective!(nodes, N, dt) ] xk1 = xk .- xk_ref1[i, :] # Array to hold the difference between variable values and their setpoints. - uk = [node[:C_a], node[:wx], node[:wy], node[:wz]] @objective(node, Min, (1 / 2 * (xk1') * Q * (xk1) + 1 / 2 * (uk') * R * (uk)) * dt) end @@ -232,11 +230,13 @@ zarray[2] = 0:10/(N-1):10 Now, let's visualize the position of the quadcopter in relation to its setpoint in each dimension. Below is the code for doing so in the x-dimension, and the code can be adapted for the y and z dimensions. ```julia -plot((1:length(xval_array)), xarray[1:end], - title = "X value over time", - xlabel = "Node (N)", - ylabel = "X Value", - label = ["Current X position" "X Setpoint"] +plot( + 1:length(xval_array), + xarray[1:end], + title = "X value over time", + xlabel = "Node (N)", + ylabel = "X Value", + label = ["Current X position" "X Setpoint"] ) ``` @@ -252,7 +252,6 @@ N = length(time_steps) dt = .5 obj_val_N = zeros(N) - for i in 1:length(time_steps) timing = @elapsed begin objval, graph, xk_ref = build_quadcopter_graph(time_steps[i], 10 / time_steps[i]); @@ -261,11 +260,14 @@ for i in 1:length(time_steps) println("Done with iteration $i after ", timing, " seconds") end -Quad_Obj_NN = plot(time_steps, obj_val_N, - title = "Objective Value vs Number of Nodes (N)", - xlabel = "Number of Nodes (N)", - ylabel = "Objective Value", - label = "Objective Value") +Quad_Obj_NN = plot( + time_steps, + obj_val_N, + title="Objective Value vs Number of Nodes (N)", + xlabel="Number of Nodes (N)", + ylabel="Objective Value", + label="Objective Value" +) ``` drawing