From afdfb9432b75ac162e366db0bcb4cd0edf22edb3 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 16 Apr 2026 18:28:12 +0200 Subject: [PATCH 1/6] Add example for problems mixing control and variable - Create docs/src/example-control-and-variable.md with two examples: * Exponential growth rate estimation with control (non-autonomous) * Harmonic oscillator pulsation optimization with control (autonomous) - Both examples demonstrate optimal control problems with both control variables and constant parameters to optimize - Update docs/make.jl to include new example in Basic Examples section - Fix heading level in example-control-free.md (## -> ###) - Remove local OptimalControl dependency from docs/src/assets/Project.toml --- docs/make.jl | 1 + docs/src/assets/Project.toml | 6 +- docs/src/example-control-and-variable.md | 375 +++++++++++++++++++++++ docs/src/example-control-free.md | 2 +- 4 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 docs/src/example-control-and-variable.md diff --git a/docs/make.jl b/docs/make.jl index 83286cfff..b26d47cbd 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -208,6 +208,7 @@ with_api_reference(src_dir, ext_dir) do api_pages "Energy minimisation" => "example-double-integrator-energy.md", "Time mininimisation" => "example-double-integrator-time.md", "Control-free problems" => "example-control-free.md", + "Control and variable" => "example-control-and-variable.md", "Singular control" => "example-singular-control.md", "State constraint" => "example-state-constraint.md", ], diff --git a/docs/src/assets/Project.toml b/docs/src/assets/Project.toml index 22c049a82..21edf0f91 100644 --- a/docs/src/assets/Project.toml +++ b/docs/src/assets/Project.toml @@ -22,7 +22,6 @@ MarkdownAST = "d0879d2d-cac2-40c8-9cee-1863dc0c7391" NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71" NLPModelsKnitro = "bec4dd0d-7755-52d5-9a02-22f0ffc7efcb" NonlinearSolve = "8913a72c-1f9b-4ce2-8d82-65094dcecaec" -OptimalControl = "5f98b655-cc9a-415a-b60e-744165666948" OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed" Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" UnoSolver = "1baa60ac-02f7-4b39-a7a8-2f4f58486b05" @@ -54,7 +53,4 @@ NonlinearSolve = "4" OrdinaryDiffEq = "6" Plots = "1" UnoSolver = "0.3" -julia = "1.10" - -[sources] -OptimalControl = {path = ".."} \ No newline at end of file +julia = "1.10" \ No newline at end of file diff --git a/docs/src/example-control-and-variable.md b/docs/src/example-control-and-variable.md new file mode 100644 index 000000000..5ffec25ce --- /dev/null +++ b/docs/src/example-control-and-variable.md @@ -0,0 +1,375 @@ +# [Problems mixing control and variable](@id example-control-and-variable) + +Problems mixing control and variable are optimal control problems that contain both a control variable and a constant parameter (variable) to optimize. They extend control-free problems by adding an explicit control input to the dynamics, while still optimizing constant parameters. + +Such problems are used for: + +- Identifying unknown parameters and control inputs simultaneously from observed data +- Finding optimal parameters and associated control laws for a given performance criterion + +This page demonstrates two examples that extend the control-free problems by adding a control input and a quadratic control cost term. + +First, we import the necessary packages: + +```@example main-growth-cv +using OptimalControl +using NLPModelsIpopt +using Plots +``` + +## Example 1: Exponential growth rate estimation with control + +Consider a system with exponential growth and an additive control: + +```math +\dot{x}(t) = \lambda \cdot x(t) + u(t), \quad x(0) = 2 +``` + +where $\lambda$ is an unknown growth rate parameter and $u(t)$ is a control input. We have observed data with some perturbations and want to estimate $\lambda$ and the optimal control $u$ by minimizing the squared error plus a quadratic control cost: + +```math +\min_{\lambda, u} \int_0^{2} \bigl( (x(t) - x_{\text{obs}}(t))^2 + \frac{1}{2} u(t)^2 \bigr) \, \mathrm{d}t +``` + +The underlying model has $\lambda = 0.5$, but the observed data includes perturbations. + +### [Problem definition](@id example-control-and-variable-problem-1) + +```@example main-growth-cv +# observed data (analytical solution with λ = 0.5) +λ_true = 0.5 +model(t) = 2 * exp(λ_true * t) +perturbation(t) = 2e-1*sin(4π*t) +data(t) = model(t) + perturbation(t) + +# optimal control problem (parameter estimation with control) +t0 = 0; tf = 2; x0 = 2 +ocp = @def begin + λ ∈ R, variable # growth rate to estimate + t ∈ [t0, tf], time + x ∈ R, state + u ∈ R, control + + x(t0) == x0 + ẋ(t) == λ * x(t) + u(t) + + ∫((x(t) - data(t))^2 + 0.5*u(t)^2) → min # fit to observed data with control cost +end +nothing # hide +``` + +### [Direct method](@id example-control-and-variable-direct-1) + +```@example main-growth-cv +direct_sol = solve(ocp; grid_size=20, display=false) +println("Estimated growth rate: λ = ", variable(direct_sol)) +println("Objective value: ", objective(direct_sol)) +nothing # hide +``` + +```@example main-growth-cv +# plot direct solution +plt = plot(direct_sol; size=(800, 600), label="Direct") + +# Add data on first plot +t_grid = time_grid(direct_sol) +plot!(plt, t_grid, data.(t_grid); subplot=1, line=:dot, lw=2, label="Data", color=:black) +``` + +The estimated parameter should be close to $\lambda \approx 0.5$. + +### [Indirect method](@id example-control-and-variable-indirect-1) + +We now solve the same problem using an indirect shooting method based on Pontryagin's Maximum Principle. First, we import the necessary packages: + +```@example main-growth-cv +using OrdinaryDiffEq # ODE solver +using NonlinearSolve # Nonlinear solver +``` + +For problems mixing control and variable, we use an **augmented Hamiltonian** approach with the maximising control. The pseudo-Hamiltonian for this problem is: + +```math +H(t, x, p, u, \lambda) = p(\lambda x + u) - (x - x_{\text{obs}}(t))^2 - \frac{1}{2} u^2 +``` + +The maximisation condition $\partial H/\partial u = 0$ gives the control in feedback form: + +```math +u(t, x, p, \lambda) = p +``` + +To handle the variable parameter $\lambda$, we treat it as an additional state with zero dynamics. This gives us the augmented system with state $(x, \lambda)$ and costate $(p, p_\lambda)$, where: + +```math +\begin{aligned} +\frac{\mathrm{d}x}{\mathrm{d}t} &= \frac{\partial H}{\partial p} = \lambda x + u \\ +\frac{\mathrm{d}\lambda}{\mathrm{d}t} &= 0 \quad \text{(constant parameter)} \\ +\frac{\mathrm{d}p}{\mathrm{d}t} &= -\frac{\partial H}{\partial x} = -p\lambda + 2(x - x_{\text{obs}}(t)) \\ +\frac{\mathrm{d}p_\lambda}{\mathrm{d}t} &= -\frac{\partial H}{\partial \lambda} = -p x +\end{aligned} +``` + +Using the maximising control $u = p$, the dynamics of $x$ becomes $\dot x = \lambda x + p$. + +The transversality condition for the variable parameter requires $p_\lambda(t_f) - p_\lambda(t_0) = 0$. Assuming $p_\lambda(t_0) = 0$, we have to satisfy: + +```math +p_\lambda(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \lambda}(t, x(t), p(t), \lambda) \, \mathrm{d}t = 0 +``` + +We use CTFlows' `augment=true` feature to automatically compute $p_\lambda(t_f)$ without manually constructing the augmented system. + +```@example main-growth-cv +# Maximising control from Hamiltonian (non-autonomous: t is required) +u(t, x, p, λ) = p + +# Create Hamiltonian flow from OCP with control +f = Flow(ocp, u) +nothing # hide +``` + +!!! note + + For more details about the flow construction, see [this page](@ref manual-flow-others). + +The shooting function enforces the transversality conditions $p(t_f) = 0$ and $p_\lambda(t_f) = 0$. Using `augment=true`, the flow automatically returns $(x(t_f), p(t_f), p_\lambda(t_f))$, with $p_\lambda(t_0) = 0$ by construction. + +```@example main-growth-cv +# Shooting function: S(p0, λ) = (p(tf), pλ(tf)) +# We want both components to be zero at tf +function shoot!(s, p0, λ) + _, px_tf, pλ_tf = f(t0, x0, p0, tf, λ; augment=true) + s[1] = px_tf + s[2] = pλ_tf + return nothing +end + +# Auxiliary in-place NLE function +nle!(s, y, _) = shoot!(s, y...) +nothing # hide +``` + +We use the direct solution to initialize the shooting method: + +```@example main-growth-cv +# Extract solution from direct method for initialization +p_direct = costate(direct_sol) +λ_direct = variable(direct_sol) + +# Initial guess +p0_guess = p_direct(t0) +λ_guess = λ_direct + +# NLE problem with initial guess (2 unknowns: p0, λ) +prob_indirect = NonlinearProblem(nle!, [p0_guess, λ_guess]) + +# Solve shooting equations +shooting_sol = solve(prob_indirect; show_trace=Val(false)) +p0_sol, λ_sol = shooting_sol.u + +println("Indirect solution:") +println("Initial costate: p0 = ", p0_sol) +println("Parameter: λ = ", λ_sol) +nothing # hide +``` + +Finally, we compute and plot the indirect solution: + +```@example main-growth-cv +# Compute and plot indirect solution +indirect_sol = f((t0, tf), x0, p0_sol, λ_sol; saveat=range(t0, tf, 200)) +plot!(plt, indirect_sol; linestyle=:dash, lw=2, label="Indirect", color=2) +``` + +The direct and indirect solutions match closely, both fitting the perturbed observed data. + +## Example 2: Harmonic oscillator pulsation optimization with control + +```@setup main-harmonic-cv +using OptimalControl +using NLPModelsIpopt +using Plots +using OrdinaryDiffEq # ODE solver +using NonlinearSolve # Nonlinear solver +``` + +Consider a harmonic oscillator with an additive control: + +```math +\ddot{q}(t) = -\omega^2 q(t) + u(t) +``` + +with initial conditions $q(0) = 1$, $\dot{q}(0) = 0$ and final condition $q(1) = 0$. We want to find the minimal pulsation $\omega$ and optimal control $u$ satisfying these constraints: + +```math + \begin{aligned} + & \text{Minimise} && \omega^2 + \frac{1}{2}\int_0^1 u(t)^2 \, \mathrm{d}t \\ + & \text{subject to} \\ + & && \ddot{q}(t) = -\omega^2 q(t) + u(t), \\[1.0em] + & && q(0) = 1, \quad \dot{q}(0) = 0, \\[0.5em] + & && q(1) = 0. + \end{aligned} +``` + +Without the control term ($u = 0$), the analytical solution is $\omega = \pi/2 \approx 1.5708$, giving $q(t) = \cos(\pi t / 2)$. + +### [Problem definition](@id example-control-and-variable-problem-2) + +```@example main-harmonic-cv +# optimal control problem (pulsation optimization with control) +q0 = 1; v0 = 0 +t0 = 0; tf = 1 +ocp = @def begin + ω ∈ R, variable # pulsation to optimize + t ∈ [t0, tf], time + x = (q, v) ∈ R², state + u ∈ R, control + + q(t0) == q0 + v(t0) == v0 + q(tf) == 0.0 # final condition + + ẋ(t) == [v(t), -ω^2 * q(t) + u(t)] + + ω^2 + 0.5∫(u(t)^2) → min # minimize pulsation with control cost +end +nothing # hide +``` + +### [Direct method](@id example-control-and-variable-direct-2) + +```@example main-harmonic-cv +direct_sol = solve(ocp; grid_size=20, display=false) +println("Optimal pulsation: ω = ", variable(direct_sol)) +println("Objective value: ", objective(direct_sol)) +nothing # hide +``` + +```@example main-harmonic-cv +plt = plot(direct_sol; size=(800, 600), label="Direct") +``` + +### [Indirect method](@id example-control-and-variable-indirect-2) + +We now solve the same problem using an indirect shooting method. For problems mixing control and variable, we use an **augmented Hamiltonian** approach with the maximising control. The pseudo-Hamiltonian for this problem is: + +```math +H(x, p, u, \omega) = p_1 v + p_2(-\omega^2 q + u) - \frac{1}{2} u^2 +``` + +The maximisation condition $\partial H/\partial u = 0$ gives the control in feedback form: + +```math +u(x, p, \omega) = p_2 +``` + +To handle the variable parameter $\omega$, we treat it as an additional state with zero dynamics. This gives us the augmented system with state $(q, v, \omega)$ and costate $(p_1, p_2, p_\omega)$, where: + +```math +\begin{aligned} +\frac{\mathrm{d}q}{\mathrm{d}t} &= \frac{\partial H}{\partial p_1} = v \\ +\frac{\mathrm{d}v}{\mathrm{d}t} &= \frac{\partial H}{\partial p_2} = -\omega^2 q + u \\ +\frac{\mathrm{d}\omega}{\mathrm{d}t} &= 0 \quad \text{(constant parameter)} \\ +\frac{\mathrm{d}p_1}{\mathrm{d}t} &= -\frac{\partial H}{\partial q} = \omega^2 p_2 \\ +\frac{\mathrm{d}p_2}{\mathrm{d}t} &= -\frac{\partial H}{\partial v} = -p_1 \\ +\frac{\mathrm{d}p_\omega}{\mathrm{d}t} &= -\frac{\partial H}{\partial \omega} = 2\omega q p_2 +\end{aligned} +``` + +Using the maximising control $u = p_2$, the dynamics of $v$ becomes $\dot v = -\omega^2 q + p_2$. + +For this problem with a Mayer cost $g(\omega) = \omega^2$, the transversality condition for the variable parameter is: + +```math +p_\omega(t_f) - p_\omega(t_0)= -\frac{\partial g}{\partial \omega} = -2\omega +``` + +Assuming $p_\omega(t_0) = 0$, we have: + +```math +p_\omega(t_f) = -\int_{t_0}^{t_f} \frac{\partial H}{\partial \omega}(t, x(t), p(t), \omega) \, \mathrm{d}t = -2\omega +``` + +We use CTFlows' `augment=true` feature to automatically compute $p_\omega(t_f)$ without manually constructing the augmented system: + +```@example main-harmonic-cv +# Maximising control from Hamiltonian +u(x, p, ω) = p[2] + +# Create Hamiltonian flow from OCP with control +f = Flow(ocp, u) +nothing # hide +``` + +!!! note + + For more details about the flow construction, see [this page](@ref manual-flow-others). + +The shooting function enforces the conditions: + +- Final condition: $q(t_f) = 0$ +- Free final velocity: $p_2(t_f) = 0$ +- Transversality condition for Mayer cost: $p_\omega(t_f) + 2\omega = 0$ + +Using `augment=true`, the flow automatically returns $(x(t_f), p(t_f), p_\omega(t_f))$, with $p_\omega(t_0) = 0$ by construction. + +```@example main-harmonic-cv +# Shooting function: S(p0, ω) +function shoot!(s, p0, ω) + x_tf, p_tf, pω_tf = f(t0, [q0, v0], p0, tf, ω; augment=true) + q_tf = x_tf[1] + pv_tf = p_tf[2] + s[1] = q_tf # q(tf) = 0 + s[2] = pv_tf # p2(tf) = 0 (free final velocity) + s[3] = pω_tf + 2ω # pω(tf) + 2ω = 0 (Mayer cost transversality) + return nothing +end + +# Auxiliary in-place NLE function +nle!(s, y, _) = shoot!(s, y[1:2], y[3]) +nothing # hide +``` + +We use the direct solution to initialize the shooting method: + +```@example main-harmonic-cv +# Extract solution from direct method for initialization +p_direct = costate(direct_sol) +ω_direct = variable(direct_sol) + +# Initial guess +p0_guess = p_direct(t0) +ω_guess = ω_direct + +# NLE problem with initial guess +prob_indirect = NonlinearProblem(nle!, [p0_guess..., ω_guess]) + +# Solve shooting equations +shooting_sol = solve(prob_indirect; show_trace=Val(false)) +p0_sol, ω_sol = shooting_sol.u[1:2], shooting_sol.u[3] + +println("Indirect solution:") +println("Initial costate: p0 = ", p0_sol) +println("Parameter: ω = ", ω_sol) +nothing # hide +``` + +Finally, we compute and plot the indirect solution: + +```@example main-harmonic-cv +# Compute and plot indirect solution +indirect_sol = f((t0, tf), [q0, v0], p0_sol, ω_sol; saveat=range(t0, tf, 200)) +plot!(plt, indirect_sol; linestyle=:dash, lw=2, label="Indirect", color=2) +``` + +The direct and indirect solutions match closely. + +!!! note "Applications" + + Problems mixing control and variable appear in many contexts: + - **System identification**: simultaneously estimating physical parameters (mass, damping, stiffness) and control inputs from experimental data + - **Optimal design**: finding optimal geometric or physical parameters (length, stiffness, etc.) together with associated control laws + - **Inverse problems**: reconstructing unknown inputs or initial conditions from partial observations while optimizing system parameters + + See the [syntax documentation](@ref manual-abstract-control-free) for more details on defining control-free problems, and the [flow documentation](@ref manual-flow-ocp) for problems with variables and controls. diff --git a/docs/src/example-control-free.md b/docs/src/example-control-free.md index 29633349b..98aa687cc 100644 --- a/docs/src/example-control-free.md +++ b/docs/src/example-control-free.md @@ -238,7 +238,7 @@ plot(direct_sol; size=(800, 400)) The optimal pulsation should be close to $\omega = \pi/2 \approx 1.5708$, and the objective $\omega^2 \approx 2.4674$. -## Comparison with analytical solutions +### Comparison with analytical solutions For the harmonic oscillator, we can compare the numerical solution with the analytical one: From 6696d909692a7545f65b95d7c38d88de892cd854 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 16 Apr 2026 18:29:10 +0200 Subject: [PATCH 2/6] Update CHANGELOG for v2.0.2 with control and variable example --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f721c031..1e2dfddf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,12 +17,19 @@ Versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Indirect methods for touch point (2-arc) and boundary arc (3-arc) cases with shooting functions - Theoretical references: Bryson et al. (1963), Jacobson et al. (1971), Bryson & Ho (1975) - Hamiltonian-based adjoint chain explanations for boundary arc dynamics + - New example for problems mixing control and variable (`example-control-and-variable.md`) with two examples: + - Exponential growth rate estimation with control (non-autonomous problem) + - Harmonic oscillator pulsation optimization with control (autonomous problem) + - Demonstrates optimal control problems with both control variables and constant parameters to optimize + - Indirect methods with augmented Hamiltonian and maximising control laws ### Changed - **Documentation organization**: - Extracted state constraint section from `example-double-integrator-energy.md` into dedicated example file - Added cross-references between energy minimization and state constraint examples + - Added "Control and variable" example to Basic Examples section in documentation navigation + - Fixed heading level in `example-control-free.md` (## -> ###) for proper hierarchy - **Dependencies**: - Updated UnoSolver from v0.2 to v0.3 From 7e380624f280f1bf0ab9ff2abbd274c9188de87e Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 16 Apr 2026 18:30:42 +0200 Subject: [PATCH 3/6] Allow workflows to run on push events, not just PRs - Update CI.yml to run on push events or when 'run ci cpu/gpu' label is present - Update Documentation.yml to run on push events or when 'run documentation' label is present --- .github/workflows/CI.yml | 4 ++-- .github/workflows/Documentation.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8400561c9..2cb295cd0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -10,7 +10,7 @@ on: jobs: test-cpu-github: - if: contains(github.event.pull_request.labels.*.name, 'run ci cpu') + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run ci cpu') uses: control-toolbox/CTActions/.github/workflows/ci.yml@main with: versions: '["1.12"]' @@ -22,7 +22,7 @@ jobs: # Job pour le runner self-hosted kkt (GPU/CUDA) test-gpu-kkt: - if: contains(github.event.pull_request.labels.*.name, 'run ci gpu') + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run ci gpu') uses: control-toolbox/CTActions/.github/workflows/ci.yml@main with: versions: '["1"]' diff --git a/.github/workflows/Documentation.yml b/.github/workflows/Documentation.yml index 5a96b2cae..df496e291 100644 --- a/.github/workflows/Documentation.yml +++ b/.github/workflows/Documentation.yml @@ -10,7 +10,7 @@ on: jobs: call: - if: contains(github.event.pull_request.labels.*.name, 'run documentation') + if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run documentation') uses: control-toolbox/CTActions/.github/workflows/documentation.yml@main with: use_ct_registry: true From dc6c0dff3545b261225484aac02772ce3ebb7541 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 16 Apr 2026 18:32:54 +0200 Subject: [PATCH 4/6] Fix Breakage workflow condition to run with 'run breakage' label --- .github/workflows/Breakage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Breakage.yml b/.github/workflows/Breakage.yml index d3af6eff1..d80669231 100644 --- a/.github/workflows/Breakage.yml +++ b/.github/workflows/Breakage.yml @@ -9,7 +9,7 @@ on: jobs: break-pkg: - if: ${{ ! contains(github.event.pull_request.labels.*.name, 'run breakage applications') }} + if: contains(github.event.pull_request.labels.*.name, 'run breakage') strategy: fail-fast: false matrix: From c0bc0748bde6628ed27e8a8467baf937f6f33bc0 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 16 Apr 2026 18:44:38 +0200 Subject: [PATCH 5/6] Split example blocks to separate solve and output - Separate solve() call from println statements in example-control-and-variable.md - Separate solve() call from println statements in example-control-free.md - Improves documentation rendering and code block organization --- docs/src/example-control-and-variable.md | 3 +++ docs/src/example-control-free.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/src/example-control-and-variable.md b/docs/src/example-control-and-variable.md index 5ffec25ce..87aeb07da 100644 --- a/docs/src/example-control-and-variable.md +++ b/docs/src/example-control-and-variable.md @@ -62,6 +62,9 @@ nothing # hide ```@example main-growth-cv direct_sol = solve(ocp; grid_size=20, display=false) +``` + +```@example main-growth-cv println("Estimated growth rate: λ = ", variable(direct_sol)) println("Objective value: ", objective(direct_sol)) nothing # hide diff --git a/docs/src/example-control-free.md b/docs/src/example-control-free.md index 98aa687cc..9ed10040f 100644 --- a/docs/src/example-control-free.md +++ b/docs/src/example-control-free.md @@ -226,6 +226,9 @@ nothing # hide ```@example main-harmonic direct_sol = solve(ocp; grid_size=20, display=false) +``` + +```@example main-harmonic println("Optimal pulsation: ω = ", variable(direct_sol)) println("Objective value: ω² = ", objective(direct_sol)) println("Expected: ω = π/2 ≈ 1.5708, ω² ≈ 2.4674") From 07663a3824948cd14a968a2f1e4baa8a4c611496 Mon Sep 17 00:00:00 2001 From: Olivier Cots Date: Thu, 16 Apr 2026 21:05:10 +0200 Subject: [PATCH 6/6] Split example blocks to separate solve and output in remaining examples - Separate solve() call from println statements in example-control-free.md (main-growth) - Separate solve() call from println statements in example-control-and-variable.md (main-harmonic-cv) - Completes the block splitting for consistency across all examples --- docs/src/example-control-and-variable.md | 3 +++ docs/src/example-control-free.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/docs/src/example-control-and-variable.md b/docs/src/example-control-and-variable.md index 87aeb07da..1343fef77 100644 --- a/docs/src/example-control-and-variable.md +++ b/docs/src/example-control-and-variable.md @@ -244,6 +244,9 @@ nothing # hide ```@example main-harmonic-cv direct_sol = solve(ocp; grid_size=20, display=false) +``` + +```@example main-harmonic-cv println("Optimal pulsation: ω = ", variable(direct_sol)) println("Objective value: ", objective(direct_sol)) nothing # hide diff --git a/docs/src/example-control-free.md b/docs/src/example-control-free.md index 9ed10040f..febfd9452 100644 --- a/docs/src/example-control-free.md +++ b/docs/src/example-control-free.md @@ -59,6 +59,9 @@ nothing # hide ```@example main-growth direct_sol = solve(ocp; grid_size=20, display=false) +``` + +```@example main-growth println("Estimated growth rate: λ = ", variable(direct_sol)) println("Objective value: ", objective(direct_sol)) nothing # hide