Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions BREAKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,52 @@

This document describes breaking changes in CTModels releases and how to migrate your code.

## [0.9.9-beta] - 2026-03-17

**No breaking changes** - This release adds flexible control interpolation with both constant and linear options while maintaining full backward compatibility.

### New Features (Non-Breaking) - 0.9.9-beta

- **Flexible Control Interpolation**
- New `control_interpolation` keyword argument in `build_solution` signatures
- Support for both `:constant` (piecewise constant) and `:linear` (piecewise linear) interpolation
- Default behavior unchanged: controls use `:constant` interpolation
- Dynamic plotting adaptation based on interpolation type

- **Enhanced Control Architecture**
- `ControlModelSolution` now includes `interpolation::Symbol` field
- New `control_interpolation(sol::Solution)` accessor method
- New `interpolation(model::ControlModelSolution)` accessor method
- `control_interpolation` added to public API exports

- **Serialization Support**
- Complete round-trip preservation of interpolation type in JSON/JLD2 formats
- Backward compatibility: existing files without interpolation field default to `:constant`
- Cross-format compatibility between JSON and JLD2 verified

### API Enhancements (Non-Breaking)

```julia
# Flexible interpolation (optional, defaults to :constant)
sol = CTModels.build_solution(ocp, T_state, T_control, T_costate, T_path, X, U, v, P;
control_interpolation=:linear) # or :constant

# Access interpolation type (new)
interp_type = CTModels.control_interpolation(sol) # Returns :constant or :linear

# Automatic plotting adaptation (enhanced)
plot(sol, :control) # Uses :steppost for constant, :path for linear
```

### Migration Notes

- **No action required** for existing code - all current behavior preserved
- **Optional enhancement**: Use `control_interpolation=:linear` for smoother control signals
- **Serialization**: Existing solution files continue to work without modification
- **Plotting**: Automatic adaptation ensures correct visualization

---

## [0.9.8-beta] - 2026-03-16

**No breaking changes** - This release adds piecewise constant interpolation for control signals while maintaining full backward compatibility.
Expand Down
56 changes: 56 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,62 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.9.9-beta] - 2026-03-17

### 🚀 Major Features

#### Flexible Control Interpolation System

- **Dual interpolation support**: Both piecewise constant (`:constant`) and piecewise linear (`:linear`) interpolation for control signals
- **Configurable interpolation**: New `control_interpolation` keyword argument in `build_solution` signatures
- **Dynamic plotting**: Automatic seriestype selection based on interpolation type (`:steppost` for constant, `:path` for linear)
- **Serialization support**: Full round-trip preservation of interpolation type in JSON/JLD2 formats
- **Backward compatibility**: Existing files without `control_interpolation` field default to `:constant`

#### Enhanced Control Architecture

- **ControlModelSolution**: Added `interpolation::Symbol` field to store interpolation type
- **Accessors**: New `control_interpolation(sol::Solution)` and `interpolation(model::ControlModelSolution)` methods
- **Default system**: Centralized `__control_interpolation()::Symbol = :constant` method for consistent defaults
- **Export system**: `control_interpolation` added to CTModels exports for public API access

### 📊 API Enhancements

```julia
# Flexible interpolation in build_solution
sol = CTModels.build_solution(ocp, T_state, T_control, T_costate, T_path, X, U, v, P;
control_interpolation=:linear) # or :constant

# Access interpolation type
interp_type = CTModels.control_interpolation(sol) # Returns :constant or :linear

# Automatic plotting adaptation
plot(sol, :control) # Uses :steppost for constant, :path for linear
```

### 🔧 Serialization & Compatibility

- **JSON/JLD2 preservation**: Interpolation type survives complete export/import cycles
- **Backward compatibility**: Files without interpolation field default to `:constant`
- **Cross-format compatibility**: JSON ↔ JLD2 interpolation preservation verified
- **Comprehensive testing**: 1751 tests passing with full serialization coverage

### 🧪 Testing & Quality

- **Comprehensive test suite**: 96 new interpolation-specific tests added
- **Integration testing**: End-to-end testing from creation to serialization to plotting
- **Compatibility testing**: Backward compatibility with existing solutions verified
- **Performance validation**: No performance impact on existing workflows

### 📝 Internal Improvements

- **Consistent defaults**: `__control_interpolation()` method used across all components
- **Clean architecture**: Separation of interpolation logic from core functionality
- **Enhanced extensions**: JSON and JLD2 extensions updated with interpolation support
- **Documentation**: Complete docstrings and examples for new features

---

## [0.9.8-beta] - 2026-03-16

### 🚀 Major Features
Expand Down
11 changes: 2 additions & 9 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "CTModels"
uuid = "34c4fa32-2049-4079-8329-de33c2a22e2d"
version = "0.9.8-beta"
version = "0.9.9-beta"
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]

[deps]
Expand Down Expand Up @@ -46,11 +46,4 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = [
"Aqua",
"JLD2",
"JSON3",
"Plots",
"Random",
"Test"
]
test = ["Aqua", "JLD2", "JSON3", "Plots", "Random", "Test"]
3 changes: 3 additions & 0 deletions ext/CTModelsJSON.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ using DocStringExtensions

using JSON3

import CTModels.OCP: __control_interpolation

# ============================================================================
# Private helpers for JSON matrix conversion
# ============================================================================
Expand Down Expand Up @@ -330,6 +332,7 @@ function CTModels.import_ocp_solution(
"control_constraints_ub_dual" => control_constraints_ub_dual,
"variable_constraints_lb_dual" => variable_constraints_lb_dual,
"variable_constraints_ub_dual" => variable_constraints_ub_dual,
"control_interpolation" => get(blob, "control_interpolation", string(__control_interpolation())),
)

# Add time grid data (format detection handled by helper)
Expand Down
3 changes: 2 additions & 1 deletion ext/plot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ function __plot_time!(
f(; kwargs...) = kwargs

# Default seriestype for controls (user can override with kwargs)
# Use :steppost for constant interpolation, :path for linear interpolation
default_seriestype = if s == :control || s == :control_norm
:steppost
CTModels.control_interpolation(sol) == :constant ? :steppost : :path
else
:path
end
Expand Down
41 changes: 35 additions & 6 deletions src/OCP/Building/solution.jl
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,27 @@ function build_solution(
variable_constraints_lb_dual::Union{Vector{Float64},Nothing}=__constraints(),
variable_constraints_ub_dual::Union{Vector{Float64},Nothing}=__constraints(),
infos::Dict{Symbol,Any}=Dict{Symbol,Any}(),
control_interpolation::Symbol=__control_interpolation(),
) where {
TX<:Union{Matrix{Float64},Function},
TU<:Union{Matrix{Float64},Function},
TP<:Union{Matrix{Float64},Function},
TPCD<:Union{Matrix{Float64},Function,Nothing},
}

# Validate control_interpolation
if control_interpolation ∉ (:constant, :linear)
throw(
Exceptions.IncorrectArgument(
"Invalid control_interpolation";
got="control_interpolation=$control_interpolation",
expected=":constant or :linear",
suggestion="Use :constant for piecewise constant (direct methods) or :linear for piecewise linear (indirect methods)",
context="build_solution parameter",
),
)
end

# get dimensions
dim_x = state_dimension(ocp)
dim_u = control_dimension(ocp)
Expand Down Expand Up @@ -235,9 +249,9 @@ function build_solution(
# Build interpolated functions for state, control, and costate
# Using unified API with validation and deepcopy+scalar wrapping
# Note: costate uses its own grid (T_costate)
# Note: control uses piecewise-constant interpolation (steppost behavior)
# Note: control uses configurable interpolation (constant for direct methods, linear for indirect methods)
fx = build_interpolated_function(X, T_state, dim_x, TX; expected_dim=dim_x)
fu = build_interpolated_function(U, T_control, dim_u, TU; expected_dim=dim_u, interpolation=:constant)
fu = build_interpolated_function(U, T_control, dim_u, TU; expected_dim=dim_u, interpolation=control_interpolation)
fp = build_interpolated_function(
P, T_costate, dim_x, TP; constant_if_two_points=true, expected_dim=dim_x
)
Expand Down Expand Up @@ -273,27 +287,27 @@ function build_solution(
allow_nothing=true,
)
# Control box constraint duals share the control grid (T_control)
# Note: use piecewise-constant interpolation like control (steppost behavior)
# Note: use same interpolation as control
fccbd = build_interpolated_function(
control_constraints_lb_dual,
T_control,
dim_control_constraints_box(ocp),
Union{Matrix{Float64},Nothing};
allow_nothing=true,
interpolation=:constant,
interpolation=control_interpolation,
)
fccud = build_interpolated_function(
control_constraints_ub_dual,
T_control,
dim_control_constraints_box(ocp),
Union{Matrix{Float64},Nothing};
allow_nothing=true,
interpolation=:constant,
interpolation=control_interpolation,
)

# build Models
state = StateModelSolution(state_name(ocp), state_components(ocp), fx)
control = ControlModelSolution(control_name(ocp), control_components(ocp), fu)
control = ControlModelSolution(control_name(ocp), control_components(ocp), fu, control_interpolation)
variable = VariableModelSolution(variable_name(ocp), variable_components(ocp), var)
dual = DualModel(
fpcd,
Expand Down Expand Up @@ -419,6 +433,7 @@ function build_solution(
variable_constraints_lb_dual::Union{Vector{Float64},Nothing}=__constraints(),
variable_constraints_ub_dual::Union{Vector{Float64},Nothing}=__constraints(),
infos::Dict{Symbol,Any}=Dict{Symbol,Any}(),
control_interpolation::Symbol=__control_interpolation(),
) where {
TX<:Union{Matrix{Float64},Function},
TU<:Union{Matrix{Float64},Function},
Expand Down Expand Up @@ -451,6 +466,7 @@ function build_solution(
variable_constraints_lb_dual=variable_constraints_lb_dual,
variable_constraints_ub_dual=variable_constraints_ub_dual,
infos=infos,
control_interpolation=control_interpolation,
)
end

Expand Down Expand Up @@ -548,6 +564,18 @@ end
"""
$(TYPEDSIGNATURES)

Return the interpolation type of the control.

# Returns
- `Symbol`: The interpolation type (`:constant` or `:linear`).
"""
function control_interpolation(sol::Solution)::Symbol
return interpolation(sol.control)
end

"""
$(TYPEDSIGNATURES)

Return the control as a function of time.

```@example
Expand Down Expand Up @@ -1464,6 +1492,7 @@ function _discretize_all_components(
return Dict{String,Any}(
"state" => _discretize_function(state(sol), T_state, dim_x),
"control" => _discretize_function(control(sol), T_control, dim_u),
"control_interpolation" => string(control_interpolation(sol)),
"costate" => _discretize_function(costate(sol), T_costate, dim_x),
"variable" => variable(sol),
"objective" => objective(sol),
Expand Down
15 changes: 15 additions & 0 deletions src/OCP/Components/control.jl
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,21 @@ end
"""
$(TYPEDSIGNATURES)

Get the interpolation type for the control.

# Arguments
- `model::ControlModelSolution`: The control model solution.

# Returns
- `Symbol`: The interpolation type (`:constant` or `:linear`).
"""
function interpolation(model::ControlModelSolution)::Symbol
return model.interpolation
end

"""
$(TYPEDSIGNATURES)

Return an empty string, since no control is defined.
"""
function name(::EmptyControlModel)::String
Expand Down
9 changes: 9 additions & 0 deletions src/OCP/Core/defaults.jl
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,12 @@ Return the default filename (without extension) for exporting and importing solu
The default value is `"solution"`.
"""
__filename_export_import() = "solution"

"""
$(TYPEDSIGNATURES)

Used to set the default value of the control interpolation type.
The default value is `:constant` for piecewise constant interpolation (direct methods).
The other possible value is `:linear` for piecewise linear interpolation (indirect methods).
"""
__control_interpolation()::Symbol = :constant
1 change: 1 addition & 0 deletions src/OCP/OCP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export is_final_time_fixed, is_final_time_free
export state_dimension, control_dimension, variable_dimension
export state_name, control_name, variable_name
export state_components, control_components, variable_components
export control_interpolation
# Constraint accessors
export path_constraints_nl, boundary_constraints_nl
export state_constraints_box, control_constraints_box, variable_constraints_box
Expand Down
11 changes: 6 additions & 5 deletions src/OCP/Types/components.jl
Original file line number Diff line number Diff line change
Expand Up @@ -146,21 +146,21 @@ end
"""
$(TYPEDEF)

Control model for a solved optimal control problem, including the control trajectory.
Represents the control trajectory in a solution.

# Fields

- `name::String`: Display name for the control variable.
- `components::Vector{String}`: Names of individual control components.
- `name::String`: Name of the control variable (e.g., `"u"`).
- `components::Vector{String}`: Names of individual control components (e.g., `["u₁", "u₂"]`).
- `value::TS`: A function `t -> u(t)` returning the control vector at time `t`.
- `interpolation::Symbol`: Interpolation type (`:constant` for piecewise constant, `:linear` for piecewise linear).

# Example

```julia-repl
julia> using CTModels

julia> u_traj = t -> [sin(t)]
julia> cms = CTModels.ControlModelSolution("u", ["u₁"], u_traj)
julia> cms = CTModels.ControlModelSolution("u", ["u₁"], u_traj, :constant)
julia> cms.value(π/2)
1-element Vector{Float64}:
1.0
Expand All @@ -170,6 +170,7 @@ struct ControlModelSolution{TS<:Function} <: AbstractControlModel
name::String
components::Vector{String}
value::TS
interpolation::Symbol
end

"""
Expand Down
2 changes: 1 addition & 1 deletion src/Serialization/Serialization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import ..CTModels.OCP
using ..OCP: AbstractModel, AbstractSolution, Solution

# Import default functions from OCP
import ..OCP: __format, __filename_export_import
import ..OCP: __format, __filename_export_import, __control_interpolation

# Define export/import tag types
include("types.jl")
Expand Down
Loading
Loading