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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,27 @@ 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.10.1] - 2026-04-22

### 🚀 Enhancements

#### Clarified :exa Backend Error Message

- **Improved error message**: `get_build_examodel` now clearly explains that the Exa (:exa) modeler is unavailable for functional API (macro-free) models
- **Actionable suggestions**: Error message now suggests using ADNLP (:adnlp) or the @def macro from CTParser.jl
- **Better context**: Error reason explains the root cause (functional API does not generate Exa builder)
- **Regression test**: Added `test_build_examodel.jl` to verify error message content

### 🐛 Bug Fixes

- Fixed misleading error message that incorrectly suggested "dynamics" instead of "Exa modeler"

## [0.10.0] - 2026-04-20

### 📦 Release

- Initial stable release version (no breaking changes from 0.9.15-beta)

## [0.9.15-beta] - 2026-04-18

### 🚀 Enhancements
Expand Down
2 changes: 1 addition & 1 deletion 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.10.0"
version = "0.10.1"
authors = ["Olivier Cots <olivier.cots@toulouse-inp.fr>"]

[deps]
Expand Down
10 changes: 5 additions & 5 deletions src/OCP/Building/model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1391,7 +1391,7 @@ end
"""
$(TYPEDSIGNATURES)

Return an error (PreconditionError) since the model is not built with the :exa backend.
Fallback method: throws a `PreconditionError` explaining that the `:exa` modeler is not available because the `Model` was assembled through the functional (macro-free) API and therefore carries no Exa builder.
"""
function get_build_examodel(
::Model{
Expand All @@ -1409,10 +1409,10 @@ function get_build_examodel(
)
throw(
Exceptions.PreconditionError(
"Cannot access dynamics";
reason="Model must be parsed with :exa backend first",
suggestion="Parse the OCP with backend=:exa before accessing dynamics",
context="dynamics accessor on unparsed model",
"The :exa modeler is not available for this model";
reason="this Model was built with the functional (macro-free) API (PreModel + time!/state!/control!/variable!/dynamics!/objective!/constraint! + build), which does not generate the Exa builder required by the Exa (:exa) modeler",
suggestion="either choose another modeler, e.g. ADNLP (:adnlp), or define the optimal control problem with the @def macro so that the Exa builder is generated",
context="get_build_examodel called on a Model built without an Exa builder",
),
)
end
Expand Down
98 changes: 98 additions & 0 deletions test/suite/ocp/test_build_examodel.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
module TestBuildExamodel

using Test: Test
import CTBase.Exceptions
using CTModels: CTModels

const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true
const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true

function test_build_examodel()
Test.@testset "Build Examodel Tests" verbose=VERBOSE showtiming=SHOWTIMING begin

# ====================================================================
# UNIT TESTS - Error on functional API model
# ====================================================================

Test.@testset "get_build_examodel error on functional API model" begin
# Build a minimal OCP using the functional (macro-free) API
ocp = CTModels.PreModel()
CTModels.time!(ocp; t0=0.0, tf=1.0)
CTModels.state!(ocp, 2)
CTModels.control!(ocp, 1)

# Simple dynamics function
dynamics!(r, t, x, u, v) = (r[1] = x[2]; r[2] = u[1])
CTModels.dynamics!(ocp, dynamics!)

# Simple objective
CTModels.objective!(ocp, :min, mayer=(x0, xf) -> xf[1]^2)

# Set time dependence (required before build)
CTModels.time_dependence!(ocp, autonomous=true)

# Build without build_examodel (functional API)
model = CTModels.build(ocp)

# Attempting to get build_examodel should throw PreconditionError
Test.@test_throws Exceptions.PreconditionError CTModels.get_build_examodel(model)

# Verify the error message contains the key information
try
CTModels.get_build_examodel(model)
Test.@test false # Should not reach here
catch err
Test.@test err isa Exceptions.PreconditionError
Test.@test occursin(":exa modeler", err.msg)
Test.@test occursin("functional", err.reason)
Test.@test occursin("macro-free", err.reason)
Test.@test occursin(":adnlp", err.suggestion)
Test.@test occursin("@def", err.suggestion)
Test.@test occursin("Exa builder", err.context)
end
end

# ====================================================================
# INTEGRATION TESTS - Verify functional API workflow
# ====================================================================

Test.@testset "Functional API workflow integration" begin
# Build a complete OCP using functional API
ocp = CTModels.PreModel()
CTModels.time!(ocp; t0=0.0, tf=1.0)
CTModels.state!(ocp, 2)
CTModels.control!(ocp, 1)
CTModels.dynamics!(ocp, (r, t, x, u, v) -> (r[1] = x[2]; r[2] = u[1]))
CTModels.objective!(ocp, :min, mayer=(x0, xf) -> xf[1]^2)

# Set time dependence (required before build)
CTModels.time_dependence!(ocp, autonomous=true)

# Build without build_examodel
model = CTModels.build(ocp)

# Verify model is built but has no Exa builder
Test.@test model isa CTModels.Model
Test.@test model.build_examodel === nothing

# Verify get_build_examodel throws informative error
try
CTModels.get_build_examodel(model)
Test.@test false # Should not reach here
catch err
Test.@test err isa Exceptions.PreconditionError
Test.@test occursin(":exa modeler", err.msg)
Test.@test occursin("functional", err.reason)
Test.@test occursin("macro-free", err.reason)
Test.@test occursin(":adnlp", err.suggestion)
Test.@test occursin("@def", err.suggestion)
Test.@test occursin("Exa builder", err.context)
end
end
end
end

end # module

# CRITICAL: Redefine in outer scope for TestRunner
test_build_examodel() = TestBuildExamodel.test_build_examodel()
Loading