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
1 change: 0 additions & 1 deletion .github/workflows/Breakage.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# Ref: https://securitylab.github.com/research/github-actions-preventing-pwn-requests
name: Breakage

# read-only repo token
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

jobs:
call:
if: github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'run ci')
uses: control-toolbox/CTActions/.github/workflows/ci.yml@main
with:
runs_on: '["ubuntu-latest", "macos-latest"]'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/Documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:

jobs:
call:
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
Expand Down
4 changes: 4 additions & 0 deletions BREAKINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ This document outlines all breaking changes introduced in CTBase v0.18.0-beta co

---

## Non-breaking note (0.18.8)

- **New exception type**: Added `SolverFailure` exception for reporting solver/integrator failures (ODE integration, optimization NLP, linear systems). Includes fields for `retcode`, `suggestion`, and `context`. No breaking changes; purely additive feature. No migration required.

## Non-breaking note (0.18.7)

- **Version stabilization**: Bumped from 0.18.6-beta to 0.18.7 for stable release. No functional changes; version promotion only.
Expand Down
35 changes: 35 additions & 0 deletions CHANGELOGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,41 @@ All notable changes to CTBase 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.18.8] - 2026-05-04

### ✨ New Features

#### **SolverFailure Exception**

- **New exception type**: Added `SolverFailure` exception for reporting solver/integrator failures across the toolbox
- **Generic retcode support**: Accommodates different solver types (SciML integrators, NLP solvers, linear solvers)
- SciML: `:Unstable`, `:DtLessThanMin`, `:MaxIters`
- NLP: `:Infeasible`, `:MaxIterations`, `:Stalled`
- Linear: condition number indicators, singular matrix flags
- **Enriched context**: Fields for `retcode`, `suggestion`, and `context` to provide actionable error information
- **User-friendly display**: Emoji-based display with 🔧 for return codes
- **Cross-package utility**: Suitable for use across CTFlows, CTDirect, and other control-toolbox packages

#### **Documentation Updates**

- **Exception guide**: Added comprehensive `SolverFailure` section to `docs/src/guide/exceptions.md`
- **Hierarchy update**: Updated exception hierarchy diagram to include `SolverFailure`
- **Quick reference**: Added `SolverFailure` to decision table for exception selection
- **Usage examples**: Provided examples for ODE integration, optimization, and linear solver failures

### 🧪 Testing

- **Comprehensive test coverage**: Added tests for `SolverFailure` in all test suites
- `test_types.jl`: Hierarchy and construction tests
- `test_display.jl`: Display tests (minimal, full fields, edge cases)
- `test_exceptions.jl`: Exception throwing and output tests
- **All tests passing**: 315 tests pass including 15 new tests for `SolverFailure`

### 📦 API Changes

- **Exception module**: Exported `SolverFailure` from `CTBase.Exceptions`
- **Display module**: Added display logic in `format_user_friendly_error` and `Base.showerror`

## [0.18.7] - 2026-03-31

### 🧹 Maintenance
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 = "CTBase"
uuid = "54762871-cc72-4466-b8e8-f6c8b58076cd"
version = "0.18.7"
version = "0.18.8"
authors = ["Olivier Cots <olivier.cots@irit.fr>", "Jean-Baptiste Caillau <caillau@univ-cotedazur.fr>"]

[deps]
Expand Down
67 changes: 66 additions & 1 deletion docs/src/guide/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ CTException (abstract)
├── NotImplemented # Unimplemented interface methods
├── ParsingError # Parsing errors
├── AmbiguousDescription # Ambiguous or incorrect descriptions
└── ExtensionError # Missing optional dependencies
├── ExtensionError # Missing optional dependencies
└── SolverFailure # Solver/integrator failures
```

## General Error Handling Pattern
Expand Down Expand Up @@ -288,6 +289,69 @@ The enriched display automatically suggests:
- Extensions are not loaded
- Weak dependencies are missing

## Solver and Integrator Exceptions

### [SolverFailure](@id solver-failure-tutorial)

```julia
CTBase.SolverFailure <: CTBase.CTException
```

**When to use**: Thrown when a solver (ODE integrator, optimization solver, linear solver, etc.)
fails to complete successfully or returns an error code.

**Fields**:

- `msg::String`: Error message describing the failure
- `retcode::Union{String,Nothing}`: Solver-specific return code (optional)
- `suggestion::Union{String,Nothing}`: How to fix the problem (optional)
- `context::Union{String,Nothing}`: Where the error occurred (optional)

**Example**:

```julia
function integrate_ode(system, integrator)
result = solve(system, integrator)
if result.retcode != :Success
throw(CTBase.SolverFailure(
"ODE integration failed",
retcode=string(result.retcode),
suggestion="Reduce time step or check initial conditions",
context="SciML integrator"
))
end
return result
end
```

The enriched display shows the solver-specific return code:

```text
❌ Error: SolverFailure, ODE integration failed
🔧 Return code: :Unstable
📂 Context: SciML integrator
💡 Suggestion: Reduce time step or check initial conditions
```

**Common return codes**:

- **SciML integrators**: `:Unstable`, `:DtLessThanMin`, `:MaxIters`, `:Success`
- **NLP solvers**: `:Infeasible`, `:MaxIterations`, `:Stalled`, `:FirstOrder`
- **Linear solvers**: Condition number indicators, singular matrix flags

**Use this exception** when:

- ODE integration fails in CTFlows
- Optimization solver does not converge in CTDirect
- Linear system is ill-conditioned
- Any numerical solver returns a failure status

**Distinction from other exceptions**:

- `IncorrectArgument`: The *input* is invalid
- `PreconditionError`: The *state* or *timing* is wrong
- `SolverFailure`: The *numerical computation* itself failed

## Quick Reference: Which Exception to Use?

| Situation | Exception | Example |
Expand All @@ -298,6 +362,7 @@ The enriched display automatically suggests:
| Parsing error | `ParsingError` | `throw(ParsingError("unexpected token", location="line 10"))` |
| Ambiguous description | `AmbiguousDescription` | `throw(AmbiguousDescription((:x,), candidates=["(:a,:b)", "(:c,:d)"]))` |
| Missing optional dependency | `ExtensionError` | `throw(ExtensionError(:Plots, feature="plotting"))` |
| Solver/integrator failure | `SolverFailure` | `throw(SolverFailure("ODE failed", retcode=":Unstable"))` |

## Enriched Error Display

Expand Down
2 changes: 1 addition & 1 deletion src/Exceptions/Exceptions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ include("display.jl")
# Export public API
export CTException
export IncorrectArgument, PreconditionError, NotImplemented, ParsingError
export AmbiguousDescription, ExtensionError
export AmbiguousDescription, ExtensionError, SolverFailure

end # module
25 changes: 25 additions & 0 deletions src/Exceptions/display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,22 @@ function format_user_friendly_error(io::IO, e::CTException)
end
end
println(io)

elseif e isa SolverFailure
# Return code
if !isnothing(e.retcode)
print(io, "🔧 Return code: ")
_print_ansi_styled(io, e.retcode, :yellow, true)
println(io)
end

if !isnothing(e.context)
println(io, "📂 Context: ", e.context)
end

if !isnothing(e.suggestion)
println(io, "💡 Suggestion: ", e.suggestion)
end
end

# Add user code location
Expand Down Expand Up @@ -293,3 +309,12 @@ Custom error display for ExtensionError.
function Base.showerror(io::IO, e::ExtensionError)
format_user_friendly_error(io, e)
end

"""
Base.showerror(io::IO, e::SolverFailure)

Custom error display for SolverFailure.
"""
function Base.showerror(io::IO, e::SolverFailure)
format_user_friendly_error(io, e)
end
68 changes: 68 additions & 0 deletions src/Exceptions/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,71 @@ struct ExtensionError <: CTException
return new(msg, weakdeps, feature, context)
end
end

"""
SolverFailure <: CTException

Exception thrown when a solver (ODE integrator, optimization solver, linear solver, etc.)
fails to complete successfully or returns an error code.

This exception is used across the Control Toolbox to report solver failures in a uniform way.
The `retcode` field is generic and can accommodate different solver types:
- SciML integrators: `:Unstable`, `:DtLessThanMin`, `:MaxIters`, `:Success`
- NLP solvers: `:Infeasible`, `:MaxIterations`, `:Stalled`, `:FirstOrder`
- Linear solvers: condition number indicators, singular matrix flags, etc.

This exception signals that the numerical computation itself failed, not that the input
was invalid (use `IncorrectArgument` for that) or that a precondition was violated (use
`PreconditionError` for that).

# Fields
- `msg::String`: Main error message describing the failure
- `retcode::Union{String, Nothing}`: Solver-specific return code (optional)
- `suggestion::Union{String, Nothing}`: How to fix the problem (optional)
- `context::Union{String, Nothing}`: Where the error occurred (optional)

# Example

```julia-repl
julia> using CTBase

julia> throw(CTBase.Exceptions.SolverFailure("ODE integration failed", retcode=":Unstable"))
ERROR: SolverFailure: ODE integration failed
```

Enhanced version with full context:

```julia
throw(CTBase.Exceptions.SolverFailure(
"Optimization solver did not converge",
retcode=":MaxIterations",
suggestion="Increase max iterations or adjust tolerance settings",
context="IPOPT solver in CTDirect"
))
```

# Common Use Cases
- ODE integration failures in CTFlows (SciML integrators)
- Non-convergence of optimization solvers in CTDirect
- Ill-conditioned linear systems in numerical algorithms
- Any numerical solver that returns a status code indicating failure

# See Also
- `IncorrectArgument`: For input validation errors
- `PreconditionError`: For precondition violations
"""
struct SolverFailure <: CTException
msg::String
retcode::Union{String,Nothing}
suggestion::Union{String,Nothing}
context::Union{String,Nothing}

function SolverFailure(
msg::String;
retcode::Union{String,Nothing}=nothing,
suggestion::Union{String,Nothing}=nothing,
context::Union{String,Nothing}=nothing,
)
new(msg, retcode, suggestion, context)
end
end
68 changes: 68 additions & 0 deletions test/suite/exceptions/test_display.jl
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ function test_exception_display()

e6 = ExtensionError(:TestExt)
@test_nowarn showerror(io, e6)

e7 = SolverFailure("Error")
@test_nowarn showerror(io, e7)
end

@testset "AmbiguousDescription - Display" begin
Expand Down Expand Up @@ -345,6 +348,71 @@ function test_exception_display()
# In a real test environment, this should show user frames
# The exact content depends on the test environment
end

@testset "SolverFailure - Display" begin
io = IOBuffer()
e = SolverFailure(
"ODE integration failed",
retcode=":Unstable",
suggestion="Reduce time step or check initial conditions",
context="SciML integrator",
)

# User-friendly
# CTBase.set_show_full_stacktrace!(false)
@test_nowarn showerror(io, e)
output = String(take!(io))
@test contains(output, "SolverFailure")
@test contains(output, "ODE integration failed")
@test contains(output, "Return code:")
@test contains(output, ":Unstable")
@test contains(output, "Suggestion:")
@test contains(output, "Reduce time step")
@test contains(output, "Context:")
@test contains(output, "SciML integrator")

# CTBase.set_show_full_stacktrace!(false)
end

@testset "SolverFailure - Missing optional fields" begin
io = IOBuffer()
# Test with only required field (msg)
e = SolverFailure("Solver failed")

# CTBase.set_show_full_stacktrace!(false)
@test_nowarn showerror(io, e)
output = String(take!(io))

@test contains(output, "SolverFailure")
@test contains(output, "Solver failed")
# Should not contain optional sections that are not provided
@test !contains(output, "Return code:")
@test !contains(output, "Context:")
@test !contains(output, "Suggestion:")
end

@testset "SolverFailure - All optional fields" begin
io = IOBuffer()
e = SolverFailure(
"Optimization did not converge";
retcode=":MaxIterations",
context="IPOPT solver",
suggestion="Increase max iterations",
)

# CTBase.set_show_full_stacktrace!(false)
@test_nowarn showerror(io, e)
output = String(take!(io))

@test contains(output, "SolverFailure")
@test contains(output, "Optimization did not converge")
@test contains(output, "Return code:")
@test contains(output, ":MaxIterations")
@test contains(output, "Context:")
@test contains(output, "IPOPT solver")
@test contains(output, "Suggestion:")
@test contains(output, "Increase max iterations")
end
end
end

Expand Down
25 changes: 25 additions & 0 deletions test/suite/exceptions/test_exceptions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,31 @@ function test_exceptions()
@test occursin("to generate documentation", output_enriched)
end

# Test SolverFailure
@testset verbose = VERBOSE showtiming = SHOWTIMING "SolverFailure" begin
e = CTBase.SolverFailure("solver failed")
@test_throws CTBase.SolverFailure throw(e)
output = sprint(showerror, e)
@test typeof(output) == String
@test occursin("SolverFailure", output)
@test occursin("solver failed", output)

# Test enriched version with retcode
e_enriched = CTBase.SolverFailure(
"ODE integration failed",
retcode=":Unstable",
suggestion="Reduce time step",
context="SciML integrator",
)
output_enriched = sprint(showerror, e_enriched)
@test occursin("Return code", output_enriched)
@test occursin(":Unstable", output_enriched)
@test occursin("Suggestion", output_enriched)
@test occursin("Reduce time step", output_enriched)
@test occursin("Context", output_enriched)
@test occursin("SciML integrator", output_enriched)
end

@testset verbose = VERBOSE showtiming = SHOWTIMING "CTException supertype catch" begin
e = CTBase.IncorrectArgument("msg")
@test_throws CTBase.IncorrectArgument throw(e)
Expand Down
Loading