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
File renamed without changes.
31 changes: 27 additions & 4 deletions CHANGELOGS.md → CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,29 @@ 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.27.4-beta] - 2026-07-10

### 🐛 Bug Fixes

#### **Core** — `to_out_of_place` produces AD-friendly functors

- **`Core.to_out_of_place` now widens the output buffer element type from the call
arguments** instead of always allocating a `Float64` buffer. The buffer type is
`promote_type(T, <arguments' element types>)`, so the resulting out-of-place function is
differentiable (its buffer can hold `ForwardDiff.Dual` numbers). The `T` keyword still
acts as a floor (default `Float64`), so plain numeric calls — including `Int` arguments —
keep their previous behaviour.
- **Why**: functors produced by `to_out_of_place` (e.g. constraint-by-label functors in
CTModels) previously threw `MethodError: Float64(::Dual)` when differentiated through
(e.g. a path constraint in a constrained CTFlows `:total` flow).
- **No breaking changes**: purely additive widening; existing behaviour on `Float64`/`Int`
arguments is unchanged. See [BREAKING.md](BREAKING.md).

#### **Housekeeping**

- Renamed `CHANGELOGS.md` → `CHANGELOG.md` and `BREAKINGS.md` → `BREAKING.md` to match the
singular naming used across the control-toolbox ecosystem (CTModels, CTFlows, CTSolvers).

## [0.27.3-beta] - 2026-07-09

### ✨ New Features
Expand Down Expand Up @@ -146,7 +169,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### ✅ Compatibility

- **No breaking changes**: purely additive. Existing code unaffected. See [BREAKINGS.md](BREAKINGS.md).
- **No breaking changes**: purely additive. Existing code unaffected. See [BREAKING.md](BREAKING.md).

## [0.26.1-beta] - 2026-07-04

Expand Down Expand Up @@ -202,7 +225,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### ✅ Compatibility

- **No breaking changes**: purely additive. Existing code unaffected. See [BREAKINGS.md](BREAKINGS.md).
- **No breaking changes**: purely additive. Existing code unaffected. See [BREAKING.md](BREAKING.md).

## [0.26.0-beta] - 2026-06-28

Expand Down Expand Up @@ -235,7 +258,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### ✅ Compatibility

- **No breaking changes**: purely additive. Existing predicates and trait families are unchanged. See [BREAKINGS.md](BREAKINGS.md).
- **No breaking changes**: purely additive. Existing predicates and trait families are unchanged. See [BREAKING.md](BREAKING.md).

## [0.25.0-beta] - 2026-06-26

Expand Down Expand Up @@ -963,7 +986,7 @@ ext/

### 🎯 Breaking Changes

See [BREAKINGS.md](BREAKINGS.md) for detailed migration guide.
See [BREAKING.md](BREAKING.md) for detailed migration guide.

### 🙏 Acknowledgments

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.27.3-beta"
version = "0.27.4-beta"
authors = ["Olivier Cots <olivier.cots@irit.fr>", "Jean-Baptiste Caillau <caillau@univ-cotedazur.fr>"]

[deps]
Expand Down
19 changes: 18 additions & 1 deletion src/Core/function_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,26 @@ julia> f = to_out_of_place(f!, 2)
julia> f(π/4) # returns approximately [0.707, 0.707]
```
"""
# Promote the element type of the numeric arguments (scalars and arrays), so the
# allocated buffer can hold e.g. ForwardDiff.Dual values during differentiation. Falls
# back to `Union{}` for non-numeric arguments (e.g. `nothing`), which is neutral under
# `promote_type`. Combined with the `T` floor below, non-numeric-only calls keep `T`.
_promote_arg_eltype() = Union{}
function _promote_arg_eltype(x::Number, rest...)
return promote_type(typeof(x), _promote_arg_eltype(rest...))
end
function _promote_arg_eltype(x::AbstractArray, rest...)
return promote_type(eltype(x), _promote_arg_eltype(rest...))
end
_promote_arg_eltype(_, rest...) = _promote_arg_eltype(rest...)

function to_out_of_place(f!, n; T=Float64)
function f(args...; kwargs...)
r = zeros(T, n)
# Widen the floor type `T` with the arguments' element types so the buffer can
# hold Dual numbers under AD; `promote_type(Float64, Int) == Float64` keeps the
# existing behaviour for plain numeric calls.
TT = promote_type(T, _promote_arg_eltype(args...))
r = zeros(TT, n)
f!(r, args...; kwargs...)
return n == 1 ? r[1] : r
end
Expand Down
21 changes: 21 additions & 0 deletions test/suite/core/test_function_utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module TestCoreFunctionUtils

using Test: Test
import CTBase.Core
import ForwardDiff

const VERBOSE = isdefined(Main, :TestData) ? Main.TestData.VERBOSE : true
const SHOWTIMING = isdefined(Main, :TestData) ? Main.TestData.SHOWTIMING : true
Expand Down Expand Up @@ -68,6 +69,26 @@ function test_function_utils()
Test.@test length(result) == 5
Test.@test result == [2.0, 4.0, 6.0, 8.0, 10.0]
end

# Regression: the allocated buffer widens its element type from the arguments so
# the out-of-place function is differentiable (buffer holds ForwardDiff.Dual).
Test.@testset "to_out_of_place - AD-friendly buffer (ForwardDiff)" begin
p!(r, x) = (r[1]=x[1]^2; r[2]=sin(x[1]); nothing)
p = Core.to_out_of_place(p!, 2)
J = ForwardDiff.jacobian(p, [1.3])
Test.@test J[1, 1] ≈ 2 * 1.3
Test.@test J[2, 1] ≈ cos(1.3)

# scalar (n=1) output differentiable via `derivative`
q!(r, x) = (r[1]=x^3; nothing)
q = Core.to_out_of_place(q!, 1)
Test.@test ForwardDiff.derivative(q, 2.0) ≈ 3 * 2.0^2

# the `T` floor is preserved: Int args keep the Float64 buffer (no narrowing)
s!(r, x) = (r[1]=x / 2; nothing)
s = Core.to_out_of_place(s!, 1)
Test.@test s(3) ≈ 1.5
end
end
return nothing
end
Expand Down
Loading