diff --git a/BREAKINGS.md b/BREAKING.md similarity index 100% rename from BREAKINGS.md rename to BREAKING.md diff --git a/CHANGELOGS.md b/CHANGELOG.md similarity index 97% rename from CHANGELOGS.md rename to CHANGELOG.md index 04fc6c07..ff5a0e94 100644 --- a/CHANGELOGS.md +++ b/CHANGELOG.md @@ -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, )`, 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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Project.toml b/Project.toml index 6d0322bb..5ba3d1a8 100644 --- a/Project.toml +++ b/Project.toml @@ -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 ", "Jean-Baptiste Caillau "] [deps] diff --git a/src/Core/function_utils.jl b/src/Core/function_utils.jl index 68988ffd..d45ebd8d 100644 --- a/src/Core/function_utils.jl +++ b/src/Core/function_utils.jl @@ -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 diff --git a/test/suite/core/test_function_utils.jl b/test/suite/core/test_function_utils.jl index 002dcaca..d411bc43 100644 --- a/test/suite/core/test_function_utils.jl +++ b/test/suite/core/test_function_utils.jl @@ -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 @@ -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