This document describes breaking changes in CTModels releases and how to migrate your code.
The following functions have been renamed to clarify that they return dual dimension, not constraint dimension:
dim_variable_constraints_box(sol)→dim_dual_variable_constraints_box(sol)dim_state_constraints_box(sol)→dim_dual_state_constraints_box(sol)dim_control_constraints_box(sol)→dim_dual_control_constraints_box(sol)
# Before (old function names)
dim = dim_state_constraints_box(sol)
# After (new function names)
dim = dim_dual_state_constraints_box(sol)The old function names were misleading because they returned the dimension of dual multipliers, not the dimension of constraints declared in the model. The new names clarify this distinction.
The functions dim_*_constraints_box(ocp::Model) (for Model, not Solution) remain unchanged and still refer to constraint dimension in the model.
The following predicate methods have been removed for PreModel and are now exclusive to Model:
is_autonomous(ocp::PreModel)- removedis_variable(ocp::PreModel)- removedis_control_free(ocp::PreModel)- removed
These methods remain available for Model instances.
# Before (PreModel access)
pre = PreModel()
state!(pre, 2)
control!(pre, 1)
variable!(pre, 2)
time_dependence!(pre; autonomous=true)
# These no longer work:
is_autonomous(pre) # MethodError
is_variable(pre) # MethodError
is_control_free(pre) # MethodError
# After (use direct field access or internal predicates)
pre.autonomous # true/false
!CTModels.OCP.__is_variable_empty(pre) # true/false
CTModels.OCP.__is_control_empty(pre) # true/falsePredicate methods are now exclusive to immutable Model types to enforce a clear separation between mutable construction (PreModel) and immutable problem definition (Model). Internal predicates (__is_*_empty) are used for construction-time checks.
The predicate methods is_autonomous(model), is_variable(model), and is_control_free(model) for Model remain unchanged and continue to work as before.
This release introduces automatic grid extension for memory optimization without breaking existing functionality:
- Automatic grid extension: Time grids that differ by only the last element are automatically extended to enable
UnifiedTimeGridModel - Memory optimization: More solutions benefit from unified grid storage instead of multiple separate grids
- Transparent behavior: Extension is automatic and requires no user intervention
- No data modification: Trajectory data matrices remain unchanged
# Before: Grids with missing last element used MultipleTimeGridModel
T_state = [0.0, 0.5, 1.0]
T_control = [0.0, 0.5] # Missing last element
# Result: MultipleTimeGridModel (separate storage)
# After: Automatic extension enables UnifiedTimeGridModel
T_state = [0.0, 0.5, 1.0]
T_control = [0.0, 0.5] # Missing last element
# Result: T_control automatically extended to [0.0, 0.5, 1.0]
# Result: UnifiedTimeGridModel (single grid storage)- No action required: Existing code continues to work unchanged
- Automatic benefit: Solutions with "almost identical" grids now automatically use unified grid model
- Same API: No changes to user-facing API; behavior is fully backward compatible
This release introduces enhancements without breaking existing functionality:
- Enhanced
time_grid()function:time_grid(sol)now works forMultipleTimeGridModelsolutions without explicit component - Default component: Automatically uses
:statewhen no component specified - Full backward compatibility: All existing code continues to work unchanged
# These still work exactly as before
time_grid(sol_unified) # UnifiedTimeGridModel (unchanged)
time_grid(sol_multi, :state) # MultipleTimeGridModel with explicit component
time_grid(sol_multi, :control) # MultipleTimeGridModel with explicit component
# This now works (previously threw IncorrectArgument)
time_grid(sol_multi) # MultipleTimeGridModel without component (NEW)- No action required: Existing code continues to work
- Optional simplification: Can use
time_grid(sol)instead oftime_grid(sol, :state)for state grid access - Explicit still preferred: Use explicit component specification when accessing non-state grids
This version includes only internal improvements and code formatting enhancements:
- Applied JuliaFormatter across entire codebase for consistent style
- Enhanced CompatHelper workflow with subdirectories support
- All existing APIs remain unchanged
- Zero functional changes - formatting and maintenance only
- Improved code maintainability and development experience
This version includes only internal improvements and documentation enhancements:
- Migration from
printstyled()to ANSI sequences for better Documenter compatibility - All existing APIs remain unchanged
- Terminal behavior is preserved
- New color support in generated documentation
No breaking changes - This release adds flexible control interpolation with both constant and linear options while maintaining full backward compatibility.
-
Flexible Control Interpolation
- New
control_interpolationkeyword argument inbuild_solutionsignatures - Support for both
:constant(piecewise constant) and:linear(piecewise linear) interpolation - Default behavior unchanged: controls use
:constantinterpolation - Dynamic plotting adaptation based on interpolation type
- New
-
Enhanced Control Architecture
ControlModelSolutionnow includesinterpolation::Symbolfield- New
control_interpolation(sol::Solution)accessor method - New
interpolation(model::ControlModelSolution)accessor method control_interpolationadded 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
# 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- No action required for existing code - all current behavior preserved
- Optional enhancement: Use
control_interpolation=:linearfor smoother control signals - Serialization: Existing solution files continue to work without modification
- Plotting: Automatic adaptation ensures correct visualization
No breaking changes - This release adds piecewise constant interpolation for control signals while maintaining full backward compatibility.
-
Piecewise Constant Interpolation
- New
ctinterpolate_constantfunction with right-continuous steppost behavior - Controls use
interpolation=:constantby default inbuild_solution - Control plotting uses
seriestype=:steppostby default - Enhanced
build_interpolated_functionwithinterpolationparameter
- New
-
Performance Improvements
- Manual interpolation implementation ~20x-8600x faster to create
- 10-21% faster for multiple evaluations
- Zero allocations for interpolation object creation
# New constant interpolation (optional)
interp = CTModels.ctinterpolate_constant(x, f)
# Enhanced interpolation helpers (backward compatible)
fu = OCP.build_interpolated_function(U, T, dim, type; interpolation=:constant)
# Control plotting improvements (automatic)
plot(sol, :control) # Now uses seriestype=:steppost by defaultNo action required - existing code continues to work unchanged.
You can now benefit from improved control interpolation:
# Existing code (still works)
sol = build_solution(ocp, T, X, U, v, P; objective=obj, ...)
# New behavior (automatic)
u = control(sol) # Now uses piecewise constant interpolation
plot(sol, :control) # Now uses steppost plotting by defaultNo breaking changes - This release adds support for optimal control problems without a control input (control_dimension == 0) while maintaining full backward compatibility.
- Zero Control Dimension Support
control!is now optional inPreModelbuild(pre)accepts models where no control has been defined- Plotting ignores
:controlwhencontrol_dimension(sol) == 0 - Serialization (JSON/JLD2) supports round-tripping solutions with empty control
No action required - existing code continues to work unchanged.
You can now write models without control!:
pre = PreModel()
time!(pre, t0=0.0, tf=1.0)
state!(pre, 2)
dynamics!(pre, (x, u) -> [x[2], -x[1]])
objective!(pre, :min, mayer=(x0, xf) -> xf[1]^2)
model = build(pre)
@assert control_dimension(model) == 0No breaking changes - This release adds a dedicated costate time grid while maintaining full backward compatibility.
-
4-Grid Time System:
build_solutionnow supports 4 independent time grids- New signature:
build_solution(ocp, T_state, T_control, T_costate, T_path, X, U, v, P; ...) - Legacy signature preserved:
build_solution(ocp, T, X, U, v, P; ...)still works - Automatic grid optimization when all grids are identical
- New signature:
-
Costate Grid Independence: Costate now has its own dedicated time grid
time_grid(sol, :costate)returns costate-specific gridclean_component_symbols((:costate,))→(:costate,)(was(:state,)before)- Enables different discretizations for state and costate (e.g., symplectic integrators)
-
Enhanced Serialization: Multi-grid format includes
time_grid_costate- Backward compatible: old files without
time_grid_costateuseT_stateas fallback - Forward compatible: new files with 4 grids work with updated readers
- Backward compatible: old files without
No action required - All existing code continues to work unchanged:
# Legacy single-grid code (still works)
sol = build_solution(ocp, T, X, U, v, P; objective=obj, ...)
# New multi-grid code (optional)
sol = build_solution(ocp, T_state, T_control, T_costate, T_path, X, U, v, P; objective=obj, ...)The package automatically detects and handles both formats. All tests pass (3324/3324).
No breaking changes - This release focuses on internal API cleanup with no impact on public functionality.
- Variable API Cleanup: Removed redundant
variable(sol::AbstractSolution)method- Eliminated potential semantic inconsistency between different AbstractSolution subtypes
- Solution concrete type already provides specific method that returns
value(sol.variable) - Test types like
DummySolution1DVarnow require explicit implementations - Enhanced method dispatch clarity and maintainability
No action required for users. All existing code continues to work unchanged. This is an internal improvement that:
- Removes a redundant generic method
- Improves type safety and method dispatch clarity
- Maintains all existing public APIs
- All tests pass (2941/2941)
No breaking changes - This release focuses on code formatting and documentation improvements with no API changes.
-
Code Formatting: Applied JuliaFormatter across entire codebase
- Consistent code style throughout all source files
- Improved readability and maintainability
- No functional changes - formatting only
-
Documentation: Fixed formatting issues in documentation files
- Resolved duplicate text in BREAKING.md
- Cleaned up markdown formatting
- Enhanced documentation consistency
No action required for users. All existing code continues to work unchanged.
No breaking changes - This release focuses on internal testing infrastructure improvements with no API changes.
- Testing Infrastructure: Complete migration to CTModels testing standards
- All test files now use
importinstead ofusingfor better module isolation - Test execution and discovery improved, but no impact on public API
- Enhanced test reliability and maintainability (internal improvement only)
- All test files now use
No action required for users. All existing code continues to work unchanged.
No breaking changes - This release adds new multi-time-grid functionality while maintaining full backward compatibility. All existing APIs continue to work unchanged.
While not breaking changes, the following new features are available:
# New time grid model access
sol = build_solution(...)
time_grid_model(sol) # Returns UnifiedTimeGridModel or MultipleTimeGridModel
# Enhanced time_grid with component specification
time_grid(sol, :state) # Component-specific time grid
time_grid(sol, :control) # Control time grid
time_grid(sol, :costate) # Costate time grid
# Multi-grid build solution (new signature)
build_solution(ocp, T_state, T_control, T_costate, T_dual, X, U, v, P; kwargs...)
# Component symbol cleaning
clean_component_symbols((:states, :controls, :constraint)) # Returns (:state, :control, :path)The serialization format has been enhanced to support multi-time-grids, but existing files remain compatible:
- Legacy Format: Automatically detected and loaded
- Multi-Grid Format: New format with component-specific time grids
- Automatic Conversion: Seamless handling of both formats
Plotting now supports additional component symbols with automatic mapping:
:control_norm→:control:path_constraint→:state:dual_path_constraint→:dual
Existing plotting code continues to work unchanged.
No breaking changes - This release only removes experimental test files from test/extras/ directory. All public APIs remain unchanged.
The InitialGuess module has been renamed to Init for better API ergonomics and more concise naming. This is a breaking change that requires users to update their imports and type references.
# Before (0.8.2-beta and earlier)
using CTModels.InitialGuess
# After (0.9.0-beta)
using CTModels.Init# Before
pre = CTModels.OptimalControlPreInit(...)
abstract_type = CTModels.AbstractOptimalControlPreInit
# After
pre = CTModels.PreInitialGuess(...)
abstract_type = CTModels.AbstractPreInitialGuessUser code changes required - update your imports and type references:
# Before
using CTModels.InitialGuess
pre_init = CTModels.OptimalControlPreInit(state=0.1, control=0.2)
# After
using CTModels.Init
pre_init = CTModels.PreInitialGuess(state=0.1, control=0.2)- More Concise API:
CTModels.InitvsCTModels.InitialGuess - Cleaner Type Names:
PreInitialGuessvsOptimalControlPreInit - Better Developer Experience: Shorter, more intuitive names
- Maintained Functionality: Zero behavioral changes, only naming improvements
- All public functions remain unchanged
- Only module and type names have been updated
- All tests pass (3146/3146)
- Ready for production use
Refactored the InitialGuess validation system to follow Single Responsibility Principle. This is an internal architectural change that does not affect the public API behavior but improves code organization.
# Before (0.8.1-beta and earlier)
# initial_guess() validated internally, build_initial_guess() had mixed responsibilities
# After (0.8.2-beta)
# initial_guess() is pure construction
# build_initial_guess() centralises validation for ALL input types# Before: This case was NOT validated (potential runtime error)
bad_init = CTModels.InitialGuess(wrong_dimensions...)
validated = CTModels.build_initial_guess(ocp, bad_init) # No validation!
# After: All branches are validated
validated = CTModels.build_initial_guess(ocp, bad_init) # Throws IncorrectArgumentNo user code changes required - this is an internal refactoring that:
- Maintains all existing public APIs
- Fixes a validation gap for direct
AbstractInitialGuessinputs - Improves error detection and code reliability
- All tests pass (147/147)
- Better Error Detection: Invalid initial guesses are caught consistently
- Cleaner Architecture: Clear separation of construction and validation concerns
- Improved Reliability: Eliminates potential runtime errors from unchecked inputs
Major refactoring where several modules have been moved from CTModels to the new CTSolvers package.
The following modules are no longer part of CTModels and must be imported from CTSolvers:
- Options →
using CTSolvers.Options - Strategies →
using CTSolvers.Strategies - Orchestration →
using CTSolvers.Orchestration - Optimization →
using CTSolvers.Optimization - Modelers →
using CTSolvers.Modelers - DOCP →
using CTSolvers.DOCP
using CTModels
using CTModels.Options
using CTModels.Strategies
using CTModels.Optimizationusing CTModels
using CTSolvers.Options
using CTSolvers.Strategies
using CTSolvers.Optimization# Before
using CTModels.Options
opt = CTModels.OptionValue(100, :user)
# After
using CTSolvers.Options
opt = CTSolvers.OptionValue(100, :user)# Before
using CTModels.Strategies
strategy = CTModels.DirectStrategy()
# After
using CTSolvers.Strategies
strategy = CTSolvers.DirectStrategy()# Before
using CTModels.Modelers
modeler = CTModels.ADNLPModeler()
# After
using CTSolvers.Modelers
modeler = CTSolvers.ADNLPModeler()# Before
using CTModels.DOCP
docp = CTModels.DiscretizedOptimalControlProblem(...)
# After
using CTSolvers.DOCP
docp = CTSolvers.DiscretizedOptimalControlProblem(...)
)If your package depends on CTModels and uses any of the moved modules, update your dependencies:
# Project.toml
[deps]
CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d"
CTSolvers = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Add CTSolvers
[compat]
CTModels = "0.8"
CTSolvers = "0.1" # Or appropriate versionThe following modules remain in CTModels and are unchanged:
- OCP: Core optimal control problem types and building
- Utils: Utility functions and helpers
- Display: Text display and printing
- Serialization: Export/import functionality
- InitialGuess: Initial guess management
- Extensions: Plots, JSON, JLD2 extensions
- CTModels 0.8.0-beta maintains compatibility with CTBase 0.18
- All remaining CTModels APIs are unchanged
- Extensions (Plots, JSON, JLD2) work as before
- Update imports: Replace CTModels module imports with CTSolvers equivalents
- Update dependencies: Add CTSolvers to your package dependencies
- Update code: Change module prefixes from
CTModels.toCTSolvers.where needed - Test: Verify your code works with the new module structure
- Check the CTSolvers documentation for detailed API
- Open an issue if you encounter migration problems
- See examples in the CTSolvers repository for usage patterns
See CHANGELOG.md for historical breaking changes.