Skip to content

Commit 020f458

Browse files
authored
Merge pull request #151 from sintefmath/lumping-for-new
Support lumping and scaling in new optimization interface + other fixes
2 parents 8f32f28 + 693fae1 commit 020f458

File tree

10 files changed

+393
-17
lines changed

10 files changed

+393
-17
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "Jutul"
22
uuid = "2b460a1a-8a2b-45b2-b125-b5c536396eb9"
33
authors = ["Olav Møyner <[email protected]>"]
4-
version = "0.4.2"
4+
version = "0.4.3"
55

66
[deps]
77
AlgebraicMultigrid = "2169fc97-5a83-5252-b627-83903c6c433c"

docs/src/usage.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ Jutul.solve_numerical_sensitivities
8484
setup_parameter_optimization
8585
```
8686

87+
### Generic optimization interface
88+
89+
```@docs
90+
DictParameters
91+
```
92+
93+
```@docs
94+
free_optimization_parameter!
95+
freeze_optimization_parameter!
96+
set_optimization_parameter!
97+
```
98+
99+
```@docs
100+
optimize
101+
parameters_gradient
102+
```
103+
87104
## Linear solvers
88105

89106
```@docs

src/DictOptimization/DictOptimization.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ module DictOptimization
55
include("validation.jl")
66
include("optimization.jl")
77
include("utils.jl")
8+
include("scaler.jl")
89
end

src/DictOptimization/interface.jl

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,55 @@
1+
"""
2+
optimized_dict = optimize(dopt, objective)
3+
optimize(dopt::DictParameters, objective, setup_fn = dopt.setup_function;
4+
grad_tol = 1e-6,
5+
obj_change_tol = 1e-6,
6+
max_it = 25,
7+
opt_fun = missing,
8+
maximize = false,
9+
simulator = missing,
10+
config = missing,
11+
solution_history = false,
12+
backend_arg = (
13+
use_sparsity = false,
14+
di_sparse = true,
15+
single_step_sparsity = false,
16+
do_prep = true,
17+
),
18+
kwarg...
19+
)
20+
21+
Optimize parameters defined in a [`DictParameters`](@ref) object using the
22+
provided objective function. At least one variable has to be declared to be free
23+
using `free_optimization_parameter!` prior to calling the optimizer.
24+
25+
# Arguments
26+
- `dopt::DictParameters`: Container with parameters to optimize
27+
- `objective`: The objective function to minimize (or maximize)
28+
- `setup_fn`: Function to set up the optimization problem. Defaults to `dopt.setup_function`
29+
30+
# Keyword Arguments
31+
- `grad_tol`: Gradient tolerance for stopping criterion
32+
- `obj_change_tol`: Objective function change tolerance for stopping criterion
33+
- `max_it`: Maximum number of iterations
34+
- `opt_fun`: Optional custom optimization function. If missing, L-BFGS will be used
35+
- `maximize`: Set to `true` to maximize the objective instead of minimizing
36+
- `simulator`: Optional simulator object used in forward simulations
37+
- `config`: Optional configuration for the setup
38+
- `solution_history`: If `true`, stores all intermediate solutions
39+
- `backend_arg`: Options for the autodiff backend:
40+
- `use_sparsity`: Enable sparsity detection for the objective function
41+
- `di_sparse`: Use sparse differentiation
42+
- `single_step_sparsity`: Enable single step sparsity detection (if sparsity does not change during timesteps)
43+
- `do_prep`: Perform preparation step
44+
45+
# Returns
46+
The optimized parameters as a dictionary.
47+
48+
# Notes
49+
- The function stores the optimization history and optimized parameters in the input `dopt` object.
50+
- If `solution_history` is `true`, intermediate solutions are stored in `dopt.history.solutions`.
51+
- The default optimization algorithm is L-BFGS with box constraints.
52+
"""
153
function optimize(dopt::DictParameters, objective, setup_fn = dopt.setup_function;
254
grad_tol = 1e-6,
355
obj_change_tol = 1e-6,
@@ -70,7 +122,7 @@ function optimize(dopt::DictParameters, objective, setup_fn = dopt.setup_functio
70122
end
71123
# Also remove AD from the internal ones and update them
72124
prm_out = deepcopy(dopt.parameters)
73-
Jutul.AdjointsDI.devectorize_nested!(prm_out, x, x_setup)
125+
optimizer_devectorize!(prm_out, x, x_setup)
74126
dopt.parameters_optimized = prm_out
75127
dopt.history = history
76128
if solution_history
@@ -81,6 +133,15 @@ function optimize(dopt::DictParameters, objective, setup_fn = dopt.setup_functio
81133
return prm_out
82134
end
83135

136+
"""
137+
parameters_gradient(dopt::DictParameters, objective, setup_fn = dopt.setup_function)
138+
139+
Compute the gradient of the objective function with respect to the parameters
140+
defined in the `DictParameters` object. This function will return the gradient
141+
as a dictionary with the same structure as the input parameters, where each
142+
entry is a vector of gradients for each parameter. Only gradients with respect
143+
to free parameters will be computed.
144+
"""
84145
function parameters_gradient(dopt::DictParameters, objective, setup_fn = dopt.setup_function;
85146
simulator = missing,
86147
config = missing,
@@ -118,6 +179,16 @@ function parameters_gradient(dopt::DictParameters, objective, setup_fn = dopt.se
118179
return out
119180
end
120181

182+
"""
183+
freeze_optimization_parameter!(dopt, "parameter_name")
184+
freeze_optimization_parameter!(dopt, ["dict_name", "parameter_name"])
185+
freeze_optimization_parameter!(dopt::DictParameters, parameter_name, val = missing)
186+
187+
Freeze an optimization parameter in the `DictParameters` object. This will
188+
remove the parameter from the optimization targets and set its value to `val` if
189+
provided. Any limits/lumping/scaling settings for this parameter will be
190+
removed.
191+
"""
121192
function freeze_optimization_parameter!(dopt::DictParameters, parameter_name, val = missing)
122193
parameter_name = convert_key(parameter_name)
123194
if !ismissing(val)
@@ -126,12 +197,62 @@ function freeze_optimization_parameter!(dopt::DictParameters, parameter_name, va
126197
delete!(dopt.parameter_targets, parameter_name)
127198
end
128199

200+
"""
201+
free_optimization_parameter!(dopt, "parameter_name", rel_min = 0.01, rel_max = 100.0)
202+
free_optimization_parameter!(dopt, ["dict_name", "parameter_name"], abs_min = -8.0, abs_max = 7.0)
203+
204+
Free an existing parameter for optimization in the `DictParameters` object. This
205+
will allow the parameter to be optimized through a call to [`optimize`](@ref).
206+
207+
# Nesting structures
208+
If your `DictParameters` has a nesting structure, you can use a vector of
209+
strings or symbols to specify the parameter name, e.g. `["dict_name",
210+
"parameter_name"]` to access the parameter located at
211+
`["dict_name"]["parameter_name"]`.
212+
213+
# Setting limits
214+
The limits can be set using the following keyword arguments:
215+
- `abs_min`: Absolute minimum value for the parameter. If not set, no absolute
216+
minimum will be applied.
217+
- `abs_max`: Absolute maximum value for the parameter. If not set, no absolute
218+
maximum will be applied.
219+
- `rel_min`: Relative minimum value for the parameter. If not set, no relative
220+
minimum will be applied.
221+
- `rel_max`: Relative maximum value for the parameter. If not set, no relative
222+
maximum will be applied.
223+
224+
For either of these entries it is possible to pass either a scalar, or an array.
225+
If an array is passed, it must have the same size as the parameter being set.
226+
227+
Note that if `dopt.strict` is set to `true`, at least one of the upper or lower
228+
bounds must be set for free parameters. If `dopt.strict` is set to `false`, the
229+
bounds are optional and the `DictParameters` object can be used to compute
230+
sensitivities, but the built-in optimization routine assumes that finite limits
231+
are set for all parameters.
232+
233+
# Other keyword arguments
234+
- `initial`: Initial value for the parameter. If not set, the current value in
235+
`dopt.parameters` will be used.
236+
- `scaler=missing`: Optional scaler for the parameter. If not set, no scaling
237+
will be applied. Available scalers are `:log`, `:exp`. The scaler will be
238+
applied
239+
- `lumping=missing`: Optional lumping array for the parameter. If not set, no
240+
lumping will be applied. The lumping array should have the same size as the
241+
parameter and contain positive integers. The lumping array defines groups of
242+
indices that should be lumped together, i.e. the same value will be used for
243+
all indices in the same group. The lumping array should contain all integers
244+
from 1 to the maximum value in the array, and all indices in the same group
245+
should have the same value in the initial parameter, otherwise an error will
246+
be thrown.
247+
"""
129248
function free_optimization_parameter!(dopt::DictParameters, parameter_name;
130249
initial = missing,
131250
abs_min = -Inf,
132251
abs_max = Inf,
133252
rel_min = -Inf,
134-
rel_max = Inf
253+
rel_max = Inf,
254+
scaler = missing,
255+
lumping = missing
135256
)
136257
parameter_name = convert_key(parameter_name)
137258
if dopt.strict
@@ -155,12 +276,30 @@ function free_optimization_parameter!(dopt::DictParameters, parameter_name;
155276
check_limit(parameter_name, initial, rel_max, is_max = true, is_rel = true)
156277
check_limit_pair(parameter_name, initial, rel_min, rel_max, is_rel = true)
157278
check_limit_pair(parameter_name, initial, abs_min, abs_max, is_rel = false)
158-
279+
if !ismissing(lumping)
280+
size(lumping) == size(initial) || error("Lumping array must have the same size as the parameter $parameter_name.")
281+
eltype(lumping) == Int || error("Lumping array must be of type Int.")
282+
minimum(lumping) >= 1 || error("Lumping array must have positive integers.")
283+
max_lumping = maximum(lumping)
284+
for i in 1:max_lumping
285+
subset = findall(isequal(i), lumping)
286+
if length(subset) == 0
287+
error("Lumping array must contain all integers from 1 to $max_lumping.")
288+
else
289+
firstval = initial[subset[1]]
290+
for j in subset
291+
if initial[j] != firstval
292+
error("Lumping array must contain the same value for all indices in the lumping group $i (value at $j differend from first value at $(subset[1])).")
293+
end
294+
end
295+
end
296+
end
297+
end
159298
targets = dopt.parameter_targets
160299
if haskey(targets, parameter_name) && dopt.verbose
161300
jutul_message("Optimization", "Overwriting limits for $parameter_name.")
162301
end
163-
targets[parameter_name] = KeyLimits(rel_min, rel_max, abs_min, abs_max)
302+
targets[parameter_name] = KeyLimits(rel_min, rel_max, abs_min, abs_max, scaler, lumping)
164303
return dopt
165304
end
166305

@@ -171,6 +310,12 @@ function free_optimization_parameters!(dopt::DictParameters, targets = all_keys(
171310
return dopt
172311
end
173312

313+
"""
314+
set_optimization_parameter!(dopt::DictParameters, parameter_name, value)
315+
316+
Set a specific optimization parameter in the `DictParameters` object. This
317+
function will update the value of the parameter in the `dopt.parameters` dictionary.
318+
"""
174319
function set_optimization_parameter!(dopt::DictParameters, parameter_name, value)
175320
set_nested_dict_value!(dopt.parameters, parameter_name, value)
176321
end

src/DictOptimization/optimization.jl

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,31 @@ function solve_and_differentiate_for_optimization(x, dopt::DictParameters, setup
77

88
prm = adj_cache[:parameters]
99
function setup_from_vector(X, step_info)
10-
# Set the parameters from the vector
11-
Jutul.AdjointsDI.devectorize_nested!(prm, X, x_setup)
10+
# if haskey(x_setup, :lumping)
11+
# X_new = similar(X, 0)
12+
# pos = 0
13+
# for (i, k) in enumerate(x_setup.names)
14+
# if haskey(x_setup.lumping, k)
15+
# L = x_setup.lumping[k]
16+
# first_index = L.first_index
17+
# N = length(first_index)
18+
# for v in L.lumping
19+
# push!(X_new, X[pos + v])
20+
# end
21+
# else
22+
# N = x_setup.offsets[i+1]-x_setup.offsets[i]
23+
# for i in pos+1:pos+N
24+
# push!(X_new, X[i])
25+
# end
26+
# end
27+
# pos += N
28+
# end
29+
# X = X_new
30+
# end
31+
# @assert length(X) == x_setup.offsets[end]-1
32+
# # Set the parameters from the vector
33+
# Jutul.AdjointsDI.devectorize_nested!(prm, X, x_setup)
34+
optimizer_devectorize!(prm, X, x_setup)
1235
# Return the case setup function
1336
# This is a function that sets up the case from the parameters
1437
F() = setup_fn(prm, step_info)
@@ -84,7 +107,33 @@ function forward_simulate_for_optimization(case, adj_cache)
84107
return Jutul.expand_to_ministeps(result)
85108
end
86109

87-
110+
function optimizer_devectorize!(prm, X, x_setup)
111+
if haskey(x_setup, :lumping) || haskey(x_setup, :scalers)
112+
X_new = similar(X, 0)
113+
pos = 0
114+
for (i, k) in enumerate(x_setup.names)
115+
scaler = get(x_setup.scalers, k, missing)
116+
if haskey(x_setup.lumping, k)
117+
L = x_setup.lumping[k]
118+
first_index = L.first_index
119+
N = length(first_index)
120+
for v in L.lumping
121+
push!(X_new, undo_scaler(X[pos + v], scaler))
122+
end
123+
else
124+
N = x_setup.offsets[i+1]-x_setup.offsets[i]
125+
for i in pos+1:pos+N
126+
push!(X_new, undo_scaler(X[i], scaler))
127+
end
128+
end
129+
pos += N
130+
end
131+
X = X_new
132+
end
133+
@assert length(X) == x_setup.offsets[end]-1
134+
# Set the parameters from the vector
135+
return Jutul.AdjointsDI.devectorize_nested!(prm, X, x_setup)
136+
end
88137

89138
function optimization_setup(dopt::DictParameters; include_limits = true)
90139
x0, x_setup = Jutul.AdjointsDI.vectorize_nested(dopt.parameters,
@@ -97,6 +146,46 @@ function optimization_setup(dopt::DictParameters; include_limits = true)
97146
else
98147
lims = missing
99148
end
149+
150+
lumping = get_lumping_for_vectorize_nested(dopt)
151+
scalers = get_scaler_for_vectorize_nested(dopt)
152+
if length(keys(lumping)) > 0 || length(keys(scalers)) > 0
153+
off = x_setup.offsets
154+
x0_new = similar(x0, 0)
155+
function push_new!(xs, sf)
156+
for xi in xs
157+
push!(x0_new, apply_scaler(xi, sf))
158+
end
159+
end
160+
pos = 0
161+
for (i, k) in enumerate(x_setup.names)
162+
x_sub = view(x0, off[i]:(off[i+1]-1))
163+
if haskey(lumping, k)
164+
x_sub = x_sub[lumping[k].first_index]
165+
end
166+
scale = get(scalers, k, missing)
167+
push_new!(x_sub, scale)
168+
if include_limits && !ismissing(scale)
169+
for index in (pos+1):(pos+length(x_sub))
170+
lims.min[index] = apply_scaler(lims.min[index], scale)
171+
lims.max[index] = apply_scaler(lims.max[index], scale)
172+
end
173+
end
174+
pos += length(x_sub)
175+
end
176+
x0 = x0_new
177+
x_setup = (
178+
offsets = x_setup.offsets,
179+
names = x_setup.names,
180+
dims = x_setup.dims,
181+
scalers = scalers,
182+
lumping = lumping
183+
)
184+
end
185+
if include_limits
186+
@assert length(lims.min) == length(lims.max) "Upper bound length ($(length(lims.max))) does not match lower bound length ($(length(lims.min)))."
187+
@assert length(lims.max) == length(x0) "Bound length ($(length(lims.max))) does not match parameter vector length ($(length(x0)))."
188+
end
100189
return (x0 = x0, x_setup = x_setup, limits = lims)
101190
end
102191

0 commit comments

Comments
 (0)