diff --git a/docs/make.jl b/docs/make.jl index 4f107581e..0a668960b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -40,7 +40,8 @@ makedocs( "Installation" => "manual/installation.md", "Examples" => [ "Linear Design" => "manual/linmpc.md", - "Nonlinear Design" => "manual/nonlinmpc.md", + "Nonlinear Design (ODE)" => "manual/nonlinmpc1.md", + "Nonlinear Design (DAE)" => "manual/nonlinmpc2.md", "ModelingToolkit" => "manual/mtk.md", ], ], diff --git a/docs/src/assets/ph_neutralization.svg b/docs/src/assets/ph_neutralization.svg new file mode 100644 index 000000000..08d106345 --- /dev/null +++ b/docs/src/assets/ph_neutralization.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/index.md b/docs/src/index.md index 7b7c5e820..a45da5459 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -33,7 +33,8 @@ Depth = 2 Pages = [ joinpath("manual", "installation.md"), joinpath("manual", "linmpc.md"), - joinpath("manual", "nonlinmpc.md"), + joinpath("manual", "nonlinmpc1.md"), + joinpath("manual", "nonlinmpc2.md"), joinpath("manual", "mtk.md") ] ``` diff --git a/docs/src/manual/mtk.md b/docs/src/manual/mtk.md index 6c3b7d1fc..1f942cf2a 100644 --- a/docs/src/manual/mtk.md +++ b/docs/src/manual/mtk.md @@ -11,8 +11,8 @@ old_logger = global_logger(); global_logger(errlogger); ## Pendulum Model -This example integrates the simple pendulum model of the [last section](@ref man_nonlin) in -[`ModelingToolkit`](https://docs.sciml.ai/ModelingToolkit/stable/) (MTK) framework and +This example integrates the simple pendulum model of the [penultimate section](@ref man_nonlin) +in [`ModelingToolkit`](https://docs.sciml.ai/ModelingToolkit/stable/) (MTK) framework and extracts appropriate `f!` and `h!` functions to construct a [`NonLinModel`](@ref). An [`NonLinMPC`](@ref) is designed from this model and simulated to reproduce the results of the last section. diff --git a/docs/src/manual/nonlinmpc.md b/docs/src/manual/nonlinmpc1.md similarity index 98% rename from docs/src/manual/nonlinmpc.md rename to docs/src/manual/nonlinmpc1.md index 5ed7e2026..730164fae 100644 --- a/docs/src/manual/nonlinmpc.md +++ b/docs/src/manual/nonlinmpc1.md @@ -1,10 +1,10 @@ -# [Manual: Nonlinear Design](@id man_nonlin) +# [Manual: Nonlinear Design (ODE)](@id man_nonlin) ```@contents -Pages = ["nonlinmpc.md"] +Pages = ["nonlinmpc1.md"] ``` -## Nonlinear Model +## Nonlinear Model (ODE) In this example, the goal is to control the angular position ``θ`` of a pendulum attached to a motor. Knowing that the manipulated input is the motor torque ``τ`` in Nm, the @@ -24,7 +24,7 @@ The following figure presents the system: border:20px solid white; display: block; margin-left: auto; margin-right: auto;"/>

``` -The plant model is nonlinear: +The plant model is nonlinear ordinary differential equation (ODE) system: ```math \begin{aligned} @@ -38,7 +38,7 @@ in which ``g`` is the gravitational acceleration in m/s², ``L``, the pendulum l the end of the pendulum in kg, all bundled in the parameter vector ``\mathbf{p} = [\begin{smallmatrix} g & L & K & m \end{smallmatrix}]'``. The [`NonLinModel`](@ref) constructor assumes by default that the state function `f` is continuous in time, that is, -an ordinary differential equation system (like here): +an ODE system (like here): ```@codeblocks line_counter = :continue diff --git a/docs/src/manual/nonlinmpc2.md b/docs/src/manual/nonlinmpc2.md new file mode 100644 index 000000000..068ec43f3 --- /dev/null +++ b/docs/src/manual/nonlinmpc2.md @@ -0,0 +1,189 @@ +# [Manual: Nonlinear Design (DAE)](@id man_dae) + +```@contents +Pages = ["nonlinmpc2.md"] +``` + +## Nonlinear Model (DAE) + +In this example, the goal is to control the pH of a solution in a continuously stirred +tank reactor (CSTR) for neutralization. The manipulated input is the inlet flow rate of +a strong base in L/min, while the inlet flow rate of a weak acid, also in L/min, is a +measured disturbance: + +```math +\begin{aligned} + \mathbf{u} &= q_{Bin} \\ + \mathbf{y} &= \mathrm{pH} \\ + \mathbf{d} &= q_{Ain} +\end{aligned} +``` + +An overflow weir draws the neutralized solution, effectively keeping a constant volume of +solution inside the tank. The following figure depicts the pH neutralization process: + +```@raw html +

ph_neutralization

+``` + +### Instantaneous Charge Balance + +The solution must remain electrically neutral, i.e. the sum of the charges of all ions must +equal zero. The ions in this case study are: + +- Hydrogen ``[\mathrm{H}^+]`` +- Sodium ``[\mathrm{Na}^+]`` +- Hydroxide ``[\mathrm{OH}^-]`` +- Acetate ``[\mathrm{Ac}^-]`` + +The instantaneous charge balance leads to: + +```math +0 = [\mathrm{H}^+] + [\mathrm{Na}^+] - [\mathrm{OH}^-] - [\mathrm{Ac}^-] +``` + +The water dissociation constant and the weak acid equilibrium constant are respectively +defined by: + +```math +\begin{aligned} +K_w &= [\mathrm{H}^+][\mathrm{OH}^-] \\ +K_a &= \frac{[\mathrm{H}^+][\mathrm{Ac}^-]}{[\mathrm{H}\mathrm{Ac}]} +\end{aligned} +``` + +We respectively denote the algebraic variable and the two states with: + +```math +\begin{aligned} + a_H &= [\mathrm{H}^+] \\ + c_A &= [\mathrm{H}\mathrm{Ac}] + [\mathrm{Ac}^-] \\ + c_B &= [\mathrm{Na}^+] +\end{aligned} +``` + +in which ``c_A`` represents the total concentration of the acid species in the reactor +(mol/L), composed of an undissociated acid ``\mathrm{H}\mathrm{Ac}`` and the acetate ion +``\mathrm{Ac}^-``. Substituting the constants, algebraic and state variables into the charge +balance leads the algebraic equation: + +```math +0 = a_H + c_B - \frac{K_w}{a_H} - \frac{K_a c_A}{K_a + a_H} +``` + +The pH is computed with: + +```math +\mathrm{pH} = -10 \log_{10}(a_H) +``` + +!!! details "Reduction to an ODE" + The algebraic equation can be further manipulated to produce this cubic expression: + ```math + 0 = a_H^3 + (c_B + K_a) a_H^2 + \big(K_a(c_A + c_B) + K_w \big) a_H - K_w K_a + ``` + We could extract the positive real root of this expression inside the output function + `h!` to transform the system to an ODE, effectively avoiding the increased complexity + of DAEs. The tutorial will still treat the system as a DAE to illustrate its API. + +### Mass Balance + +Applying a mass balance on the weak acid ``A`` and the strong base ``B`` invariants leads +to the differential equations: + +```math +\begin{aligned} + \dot{c}_A(t) &= \frac{60}{V}(q_{Ain} c_{Ain} - q_{out} c_{Aout}) \\ + \dot{c}_B(t) &= \frac{60}{V}(q_{Bin} c_{Bin} - q_{out} c_{Bout}) +\end{aligned} +``` + +in which the concentrations ``c`` are in mol/L, the tank volume ``V`` in L and the +volumetric flow rates ``q`` in L/min. The accumulation terms ``\dot{c}`` are in mol/(L h) +because of the 60 factors. By assuming a perfectly mixed reactor and a constant volume +because of the weir, the following relations compute the outflow terms: + +```math +\begin{aligned} + c_{Aout} &= c_{A} \\ + c_{Bout} &= c_{B} \\ + q_{out} &= q_{Ain} + q_{Bin} +\end{aligned} +``` + +The code is: + +```@example 1 +using ModelPredictiveControl + +V = 1000.0 # reactor volume [L] +c_Ain = 0.1 # feed concentration of weak acid [mol/L] +c_Bin = 0.1 # feed concentration of strong base [mol/L] +Kw = 1.0e-14 # water dissociation constant [mol^2/L^2] +Ka = 1.75e-5 # acid dissociation constant [mol/L] + +function fq!(ẋ, res, x, a, u, d, p) + c_Ain, c_Bin, Kw, Ka, V = p + q_Ain, q_Bin = d[1], u[1] # [L/min], [L/min] + c_A, c_B = x[1], x[2] # [mol/L], [mol/L] + a_H = a[1] # [mol/L] + q_out = q_Ain + q_Bin # [L/min] + c_Aout = c_A # [mol/L] + c_Bout = c_B # [mol/L] + ẋ[1] = (60/V)*(q_Ain * c_Ain - q_out * c_Aout) + ẋ[2] = (60/V)*(q_Bin * c_Bin - q_out * c_Bout) + res[1] = a_H + c_B - (Kw / a_H) - (Ka * c_A / (Ka + a_H)) + return nothing +end + +function h!(y, _, a, _ , _ ) + a_H = a[1] + pH = try + -log10(a_H) + catch myerror + myerror isa DomainError ? NaN : rethrow() + end + y[1] = pH + return nothing +end + +Ts = 0.5 # Sample time [h] +nu, nx, na, ny, nd = 1, 2, 1, 1, 1 +p = [c_Ain, c_Bin, Kw, Ka, V] + +model = NonLinModelDAE(fq!, h!, Ts, nu, nx, na, ny, nd; p, as_0=[1e-5]) +vu, vd = ["\$q_B\$ (L/min)"], ["\$q_A\$ (L/min)"] +vx, vy = ["\$c_A\$ (mol/L)", "\$c_B\$ (mol/L)"], ["\$\\mathrm{pH}\$"] +model = setname!(model, u=vu, x=vx, y=vy, d=vd) + +u = [10.0] +d = [10.0] +x_0 = [0.051, 0.049] +N = 61 +Y_data, U_data, D_data, X_data = zeros(ny, N), zeros(nu, N), zeros(nd, N), zeros(nx, N) +x = x_0 +let x=x, u=u, d=d + setstate!(model, x) + for i=1:N + d = i ≤ 2N÷3 ? [10.0] : [9.8] + y = model(d) + u = i ≤ N÷3 ? [10.0] : [9.7] + Y_data[:, i] = y + U_data[:, i] = u + D_data[:, i] = d + X_data[:, i] = x + x = updatestate!(model, u, d) + end +end +res = SimResult(model, U_data, Y_data, D_data; X_data) + +using Plots +#theme(:default) +theme(:dark) +default(fontfamily="Computer Modern"); scalefontsizes(1.1) +#p = plot(res, plotx=true, plotd=false, xlabel="Time (h)") +#xlabel!(p[3], "") +p = plot(res, plotd=true, xlabel="Time (h)") +```