Skip to content

Commit c019139

Browse files
authored
Merge branch 'master' into hash_default
2 parents 0e15bc1 + bc05728 commit c019139

15 files changed

+747
-145
lines changed

.github/workflows/CI.yml

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,12 @@ jobs:
5454
julia --color=yes --threads=auto --check-bounds=yes --depwarn=yes --code-coverage=user -e 'import Coverage; import Pkg; Pkg.activate("."); Pkg.test(coverage=true)'
5555
julia --color=yes coverage.jl
5656
shell: bash
57-
- uses: codecov/codecov-action@v5
57+
- name: Upload coverage artifact
58+
if: success()
59+
uses: actions/upload-artifact@v6
5860
with:
59-
token: ${{ secrets.CODECOV_TOKEN }}
60-
files: lcov.info
61+
name: coverage-${{ matrix.os }}-julia-${{ matrix.julia-version }}
62+
path: lcov.info
6163

6264

6365
additional_tests:
@@ -89,8 +91,74 @@ jobs:
8991
SR_TEST=${{ matrix.test_name }} julia --color=yes --threads=auto --check-bounds=yes --depwarn=yes --code-coverage=user -e 'import Coverage; import Pkg; Pkg.activate("."); Pkg.test(coverage=true)'
9092
julia --color=yes coverage.jl
9193
shell: bash
92-
- uses: codecov/codecov-action@v5
94+
- name: Upload coverage artifact
9395
if: steps.run-tests.outcome == 'success'
96+
uses: actions/upload-artifact@v6
97+
with:
98+
name: coverage-${{ matrix.test_name }}-${{ matrix.os }}-julia-${{ matrix.julia-version }}
99+
path: lcov.info
100+
101+
102+
optim_v1_smoketest:
103+
name: Optim v1 (NLSolversBase v7) - ubuntu-latest
104+
runs-on: ubuntu-latest
105+
timeout-minutes: 60
106+
steps:
107+
- uses: actions/checkout@v4
108+
- uses: julia-actions/setup-julia@v2
109+
with:
110+
version: '1'
111+
- uses: julia-actions/cache@v2
112+
- uses: julia-actions/julia-buildpkg@v1
113+
- name: Pin Optim v1 + NLSolversBase v7
114+
run: |
115+
julia --color=yes -e 'import Pkg; Pkg.add("Coverage")'
116+
julia --color=yes -e 'import Pkg; Pkg.activate("."); Pkg.add(Pkg.PackageSpec(name="Optim", version="1")); Pkg.add(Pkg.PackageSpec(name="NLSolversBase", version="7")); Pkg.status(["Optim", "NLSolversBase"])'
117+
shell: bash
118+
- name: Run Optim tests (with coverage)
119+
id: run-tests
120+
run: |
121+
SR_TEST=optim julia --color=yes --threads=auto --check-bounds=yes --depwarn=yes --code-coverage=user -e 'import Coverage; import Pkg; Pkg.activate("."); Pkg.test(coverage=true)'
122+
julia --color=yes coverage.jl
123+
shell: bash
124+
- name: Upload coverage artifact
125+
if: steps.run-tests.outcome == 'success'
126+
uses: actions/upload-artifact@v6
127+
with:
128+
name: coverage-optim-v1-${{ runner.os }}-julia-1
129+
path: lcov.info
130+
131+
132+
codecov:
133+
name: Upload combined coverage to Codecov
134+
runs-on: ubuntu-latest
135+
needs:
136+
- test
137+
- additional_tests
138+
- optim_v1_smoketest
139+
steps:
140+
# Codecov uploader expects a git checkout (commit metadata + repo root)
141+
- uses: actions/checkout@v4
142+
143+
- name: Download coverage artifacts
144+
uses: actions/download-artifact@v7
145+
with:
146+
pattern: coverage-*
147+
path: coverage
148+
149+
- name: Merge lcov files
150+
run: |
151+
set -euxo pipefail
152+
cd coverage
153+
find . -name 'lcov.info' -print
154+
cat $(find . -name 'lcov.info' -print | sort) > merged-lcov.info
155+
# Normalize Windows path separators so Codecov can match sources.
156+
sed -i -e '/^SF:/ s#\\\\#/#g' merged-lcov.info
157+
wc -l merged-lcov.info
158+
159+
- name: Upload to Codecov
160+
uses: codecov/codecov-action@v5
94161
with:
95162
token: ${{ secrets.CODECOV_TOKEN }}
96-
files: lcov.info
163+
files: coverage/merged-lcov.info
164+
disable_search: true

Project.toml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "DynamicExpressions"
22
uuid = "a40a106e-89c9-4ca8-8020-a735e8728b6b"
33
authors = ["MilesCranmer <miles.cranmer@gmail.com>"]
4-
version = "2.4.0"
4+
version = "2.5.1"
55

66
[deps]
77
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
@@ -18,13 +18,14 @@ TOML = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
1818
Bumper = "8ce10254-0962-460f-a3d8-1f77fea1446e"
1919
LoopVectorization = "bdcacae8-1622-11e9-2a5c-532679323890"
2020
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
21+
NLSolversBase = "d41bc354-129a-5804-8e4c-c37616107c6c"
2122
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
2223
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"
2324

2425
[extensions]
2526
DynamicExpressionsBumperExt = "Bumper"
2627
DynamicExpressionsLoopVectorizationExt = "LoopVectorization"
27-
DynamicExpressionsOptimExt = "Optim"
28+
DynamicExpressionsOptimExt = ["Optim", "NLSolversBase"]
2829
DynamicExpressionsSymbolicUtilsExt = "SymbolicUtils"
2930
DynamicExpressionsZygoteExt = "Zygote"
3031

@@ -36,16 +37,20 @@ DispatchDoctor = "0.4"
3637
Interfaces = "0.3"
3738
LoopVectorization = "0.12"
3839
MacroTools = "0.4, 0.5"
39-
Optim = "0.19, 1"
40+
Optim = "1, 2"
41+
NLSolversBase = "7, 8"
4042
PrecompileTools = "1"
4143
Reexport = "1"
42-
SymbolicUtils = "0.19, ^1.0.5, 2, 3"
44+
SymbolicUtils = "4"
4345
Zygote = "0.7"
4446
julia = "1.10"
47+
Random = "1"
48+
TOML = "1"
4549

4650
[extras]
4751
Bumper = "8ce10254-0962-460f-a3d8-1f77fea1446e"
4852
LoopVectorization = "bdcacae8-1622-11e9-2a5c-532679323890"
4953
Optim = "429524aa-4258-5aef-a3af-852621145aeb"
54+
NLSolversBase = "d41bc354-129a-5804-8e4c-c37616107c6c"
5055
SymbolicUtils = "d1185830-fcd6-423d-90d6-eec64667417b"
5156
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"

ext/DynamicExpressionsOptimExt.jl

Lines changed: 115 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ using DynamicExpressions:
99
set_scalar_constants!,
1010
get_number_type
1111

12-
import Optim: Optim, OptimizationResults, NLSolversBase
12+
import Optim: Optim, OptimizationResults
13+
using NLSolversBase: NLSolversBase
1314

1415
#! format: off
1516
"""
@@ -38,41 +39,136 @@ function Optim.minimizer(r::ExpressionOptimizationResults)
3839
end
3940

4041
"""Wrap function or objective with insertion of values of the constant nodes."""
41-
function wrap_func(
42+
@inline function _wrap_objective_x_last(
43+
::Nothing, tree::N, refs
44+
) where {N<:Union{AbstractExpressionNode,AbstractExpression}}
45+
return nothing
46+
end
47+
@inline function _wrap_objective_x_last(
4248
f::F, tree::N, refs
4349
) where {F<:Function,T,N<:Union{AbstractExpressionNode{T},AbstractExpression{T}}}
4450
function wrapped_f(args::Vararg{Any,M}) where {M}
45-
first_args = args[begin:(end - 1)]
46-
x = args[end]
51+
x = args[M]
4752
set_scalar_constants!(tree, x, refs)
48-
return @inline(f(first_args..., tree))
53+
newargs = Base.setindex(args, tree, M)
54+
return @inline(f(newargs...))
4955
end
50-
# without first args, it looks like this
51-
# function wrapped_f(x)
52-
# set_scalar_constants!(tree, x, refs)
53-
# return @inline(f(tree))
54-
# end
5556
return wrapped_f
5657
end
58+
59+
@inline function _wrap_objective_xv_tail(
60+
::Nothing, tree::N, refs
61+
) where {N<:Union{AbstractExpressionNode,AbstractExpression}}
62+
return nothing
63+
end
64+
@inline function _wrap_objective_xv_tail(
65+
f::F, tree::N, refs
66+
) where {F<:Function,T,N<:Union{AbstractExpressionNode{T},AbstractExpression{T}}}
67+
function wrapped_f(args::Vararg{Any,M}) where {M}
68+
if M < 2
69+
throw(
70+
ArgumentError(
71+
"Expected at least 2 arguments for objective functions of the form (..., x, v).",
72+
),
73+
)
74+
end
75+
x = args[M - 1]
76+
set_scalar_constants!(tree, x, refs)
77+
newargs = Base.setindex(args, tree, M - 1)
78+
return @inline(f(newargs...))
79+
end
80+
return wrapped_f
81+
end
82+
83+
function wrap_func(
84+
f::F, tree::N, refs
85+
) where {F<:Function,T,N<:Union{AbstractExpressionNode{T},AbstractExpression{T}}}
86+
return _wrap_objective_x_last(f, tree, refs)
87+
end
5788
function wrap_func(
5889
::Nothing, tree::N, refs
5990
) where {N<:Union{AbstractExpressionNode,AbstractExpression}}
6091
return nothing
6192
end
93+
94+
# `NLSolversBase.InplaceObjective` is an internal type whose field layout changed
95+
# between NLSolversBase versions (and therefore between Optim majors).
96+
#
97+
# This extension supports:
98+
# - Optim v1.x (NLSolversBase v7.x): df, fdf, fgh, hv, fghv
99+
# - Optim v2.x (NLSolversBase v8.x): fdf, fgh, hvp, fghvp, fjvp
100+
#
101+
# We store the fields both as symbols (for runtime layout checks) and as `Val`s
102+
# (so the wrapper construction is type-stable and can compile-in the field set).
103+
const _INPLACEOBJECTIVE_SPEC_V8 = (
104+
field_syms=(:fdf, :fgh, :hvp, :fghvp, :fjvp),
105+
fields=(Val(:fdf), Val(:fgh), Val(:hvp), Val(:fghvp), Val(:fjvp)),
106+
x_last=(Val(:fdf), Val(:fgh)),
107+
xv_tail=(Val(:hvp), Val(:fghvp), Val(:fjvp)),
108+
)
109+
const _INPLACEOBJECTIVE_SPEC_V7 = (
110+
field_syms=(:df, :fdf, :fgh, :hv, :fghv),
111+
fields=(Val(:df), Val(:fdf), Val(:fgh), Val(:hv), Val(:fghv)),
112+
x_last=(Val(:df), Val(:fdf), Val(:fgh)),
113+
xv_tail=(Val(:hv), Val(:fghv)),
114+
)
115+
116+
@inline function _wrap_inplaceobjective_field(
117+
v_field::Val{field}, f::NLSolversBase.InplaceObjective, tree::N, refs, spec
118+
) where {field,N<:Union{AbstractExpressionNode,AbstractExpression}}
119+
if v_field in spec.x_last
120+
return _wrap_objective_x_last(getfield(f, field), tree, refs)
121+
elseif v_field in spec.xv_tail
122+
return _wrap_objective_xv_tail(getfield(f, field), tree, refs)
123+
else
124+
throw(
125+
ArgumentError(
126+
"Internal error: no wrapping rule for InplaceObjective field $(field). " *
127+
"Please open an issue at github.com/SymbolicML/DynamicExpressions.jl with your versions.",
128+
),
129+
)
130+
end
131+
end
132+
133+
@inline function _wrap_inplaceobjective(
134+
f::NLSolversBase.InplaceObjective, tree::N, refs, spec
135+
) where {N<:Union{AbstractExpressionNode,AbstractExpression}}
136+
wrapped = map(spec.fields) do v_field
137+
_wrap_inplaceobjective_field(v_field, f, tree, refs, spec)
138+
end
139+
return NLSolversBase.InplaceObjective(wrapped...)
140+
end
141+
62142
function wrap_func(
63143
f::NLSolversBase.InplaceObjective, tree::N, refs
64144
) where {N<:Union{AbstractExpressionNode,AbstractExpression}}
65-
# Some objectives, like `Optim.only_fg!(fg!)`, are not functions but instead
145+
# Some objectives, like `only_fg!(fg!)`, are not functions but instead
66146
# `InplaceObjective`. These contain multiple functions, each of which needs to be
67147
# wrapped. Some functions are `nothing`; those can be left as-is.
68-
@assert fieldnames(NLSolversBase.InplaceObjective) == (:df, :fdf, :fgh, :hv, :fghv)
69-
return NLSolversBase.InplaceObjective(
70-
wrap_func(f.df, tree, refs),
71-
wrap_func(f.fdf, tree, refs),
72-
wrap_func(f.fgh, tree, refs),
73-
wrap_func(f.hv, tree, refs),
74-
wrap_func(f.fghv, tree, refs),
75-
)
148+
#
149+
# We use `@static` branching so that only the relevant layout for the *installed*
150+
# NLSolversBase version is compiled/instrumented.
151+
@static if fieldnames(NLSolversBase.InplaceObjective) ==
152+
_INPLACEOBJECTIVE_SPEC_V8.field_syms
153+
# NLSolversBase v8 / Optim v2
154+
return _wrap_inplaceobjective(f, tree, refs, _INPLACEOBJECTIVE_SPEC_V8)
155+
elseif fieldnames(NLSolversBase.InplaceObjective) ==
156+
_INPLACEOBJECTIVE_SPEC_V7.field_syms
157+
# NLSolversBase v7 / Optim v1
158+
return _wrap_inplaceobjective(f, tree, refs, _INPLACEOBJECTIVE_SPEC_V7)
159+
# (Optim < 1 is no longer supported.)
160+
else
161+
# LCOV_EXCL_START
162+
fields = fieldnames(NLSolversBase.InplaceObjective)
163+
throw(
164+
ArgumentError(
165+
"Unsupported NLSolversBase.InplaceObjective field layout: $(fields). " *
166+
"This extension supports layouts used by NLSolversBase v7 (Optim v1) and v8 (Optim v2). " *
167+
"Please open an issue at github.com/SymbolicML/DynamicExpressions.jl with your versions.",
168+
),
169+
)
170+
# LCOV_EXCL_END
171+
end
76172
end
77173

78174
"""

0 commit comments

Comments
 (0)