Skip to content

Commit 20c1162

Browse files
authored
Improve the Extensibility of Derivative Methods (#345)
* initial commit * added docstring * added docstrings and tests * add more docs and tests * Fix bugs * minor fixes * update doc test * fix spacing * fix doc references
1 parent 0edc47e commit 20c1162

File tree

7 files changed

+409
-230
lines changed

7 files changed

+409
-230
lines changed

docs/src/develop/extensions.md

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,11 @@ we provide an API to do just this. A complete template is provided in
213213
to help streamline this process. The extension steps are:
214214
1. Define the new method `struct` that inherits from the correct
215215
[`AbstractDerivativeMethod`](@ref) subtype
216-
2. Extend [`allows_high_order_derivatives`](@ref)
216+
2. Extend [`InfiniteOpt.allows_high_order_derivatives`](@ref)
217217
3. Extend [`InfiniteOpt.generative_support_info`](@ref InfiniteOpt.generative_support_info(::AbstractDerivativeMethod))
218218
if the method is a [`GenerativeDerivativeMethod`](@ref)
219-
4. Extend [`InfiniteOpt.evaluate_derivative`](@ref).
219+
4. Extend [`InfiniteOpt.derivative_expr_data`](@ref)
220+
5. Extend [`InfiniteOpt.make_indexed_derivative_expr`](@ref).
220221

221222
To exemplify this process let's implement 1st order explicit Euler which is already
222223
implemented via `FiniteDifference(Forward())`, but let's make our own anyway for
@@ -252,50 +253,66 @@ InfiniteOpt.allows_high_order_derivatives(::ExplicitEuler) = false
252253
253254
```
254255
Conversely, we could set the output to `true` if we wanted to directly support higher
255-
order derivatives. In which case, we would need to query [`derivative_order`](@ref)
256-
in [`InfiniteOpt.evaluate_derivative`](@ref) and account for the order as needed.
256+
order derivatives. In which case, we would need to take the order into account in steps 4 and 5.
257257

258258
Since, this is a `NonGenerativeDerivativeMethod` we skip step 3. This is
259259
however exemplified in the extension template.
260260

261-
Now we just need to do step 4 which is to extend
262-
[`InfiniteOpt.evaluate_derivative`](@ref). This function generates all the
261+
For step 4, we extend [`InfiniteOpt.derivative_expr_data`](@ref).
262+
This function generates all the needed data to make the
263263
expressions necessary to build the derivative evaluation equations (derivative
264264
constraints). We assume these relations to be of the form ``h = 0`` where ``h``
265-
is a vector of expressions and is what the output of
266-
`InfiniteOpt.evaluate_derivative` should be. Thus, mathematically ``h`` should
267-
be of the form:
265+
is a vector of expressions. Thus, mathematically ``h`` should be of the form:
268266
```math
269267
\begin{aligned}
270-
&&& y(t_{1}) - y(0) - (t_{1} - t_{0})\frac{d y(0)}{dt} \\
268+
&&& y(t_{2}) - y(t_{1}) - (t_{2} - t_{1})\frac{d y(t_1)}{dt} \\
271269
&&& \vdots \\
272270
&&& y(t_{n+1}) - y(t_n) - (t_{n+1} - t_{n})\frac{d y(t_n)}{dt} \\
273-
&&& \vdots \\
274-
&&& y(t_{k}) - y(k-1) - (t_{k} - t_{k-1})\frac{d y(k-1)}{dt} \\
275271
\end{aligned}
276272
```
277-
With this in mind let's now extend `InfiniteOpt.evaluate_derivative`:
273+
The required data must include the support indices used for each derivative
274+
variable and then any other constants needed. In this case, we will need the
275+
indices ``\{1, \dots, n\}`` and no additional data (additional data is exemplified
276+
in the extension template). With this in mind let's now extend
277+
`InfiniteOpt.derivative_expr_data`:
278278
```jldoctest deriv_ext; output = false
279-
function InfiniteOpt.evaluate_derivative(
279+
function InfiniteOpt.derivative_expr_data(
280280
dref::GeneralVariableRef,
281-
vref::GeneralVariableRef, # the variable that the derivative acts on
282-
method::ExplicitEuler,
283-
write_model::JuMP.AbstractModel
281+
order::Int,
282+
supps::Vector{Float64},
283+
method::ExplicitEuler
284284
)
285-
# get the basic derivative information
286-
pref = operator_parameter(dref)
287-
# generate the derivative expressions h_i corresponding to the equations of
288-
# the form h_i = 0
289-
supps = supports(pref, label = All)
290-
exprs = Vector{JuMP.AbstractJuMPScalar}(undef, length(supps) - 1)
291-
for i in eachindex(exprs)
292-
d = InfiniteOpt.make_reduced_expr(dref, pref, supps[i], write_model)
293-
v1 = InfiniteOpt.make_reduced_expr(vref, pref, supps[i], write_model)
294-
v2 = InfiniteOpt.make_reduced_expr(vref, pref, supps[i + 1], write_model)
295-
change = supps[i + 1] - supps[i]
296-
exprs[i] = JuMP.@expression(write_model, v2 - v1 - change * d)
297-
end
298-
return exprs
285+
# generate the support indices to be used for each call of `make_indexed_derivative_expr`
286+
idxs = 1:length(supps)-1
287+
# return the indexes and the other iterators
288+
return (idxs, ) # output must be a tuple
289+
end
290+
291+
# output
292+
293+
294+
```
295+
296+
Finally, we just need to extend [`InfiniteOpt.make_indexed_derivative_expr`](@ref).
297+
This will be used to create derivative expressions for each index determined (and additional datum) produced by `derivative_expr_data`.
298+
```jldoctest deriv_ext; output = false
299+
function InfiniteOpt.make_indexed_derivative_expr(
300+
dref::GeneralVariableRef,
301+
vref::GeneralVariableRef,
302+
pref::GeneralVariableRef,
303+
order::Int,
304+
idx,
305+
supps::Vector{Float64}, # ordered
306+
write_model::JuMP.AbstractModel,
307+
::ExplicitEuler,
308+
# put extra data args here (none in this case)
309+
)
310+
# generate the derivative expression h corresponding to the equation of
311+
# the form h = 0
312+
d = InfiniteOpt.make_reduced_expr(dref, pref, supps[idx], write_model)
313+
v1 = InfiniteOpt.make_reduced_expr(vref, pref, supps[idx], write_model)
314+
v2 = InfiniteOpt.make_reduced_expr(vref, pref, supps[idx + 1], write_model)
315+
return JuMP.@expression(write_model, -(supps[idx+1] - supps[idx]) * d + v2 - v1)
299316
end
300317
301318
# output
@@ -304,10 +321,14 @@ end
304321
```
305322
We used [`InfiniteOpt.make_reduced_expr`](@ref) as a convenient helper function
306323
to generate the semi-infinite variables/expressions we need to generate at each
307-
support point. Also note that [`InfiniteOpt.add_generative_supports`](@ref) needs
308-
to be included for `GenerativeDerivativeMethods`, but is not necessary in this
309-
example. We also would have needed to query [`derivative_order`](@ref) and take it
310-
into account if we had selected this method to support higher order derivatives.
324+
support point.
325+
326+
!!! note
327+
If your new derivative method is not compatible can not be broken
328+
up into the `derivative_expr_data`-`make_indexed_derivative_expr`
329+
workflow, then you can instead extend [`InfiniteOpt.evaluate_derivative`](@ref).
330+
This is discouraged where possible since it may make your method incompatible
331+
with backends that depend on the preferred workflow.
311332

312333
Now that we have completed all the necessary steps, let's try it out!
313334
```jldoctest deriv_ext
@@ -324,8 +345,8 @@ julia> evaluate(dy)
324345
325346
julia> derivative_constraints(dy)
326347
2-element Vector{InfOptConstraintRef}:
327-
y(5) - y(0) - 5 d/dt[y(t)](0) = 0.0
328-
y(10) - y(5) - 5 d/dt[y(t)](5) = 0.0
348+
-5 d/dt[y(t)](0) + y(5) - y(0) = 0
349+
-5 d/dt[y(t)](5) + y(10) - y(5) = 0
329350
```
330351
We implemented explicit Euler and it works! Now go and extend away!
331352

docs/src/guide/derivative.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -464,10 +464,10 @@ julia> evaluate(d1)
464464
465465
julia> derivative_constraints(d1)
466466
4-element Vector{InfOptConstraintRef}:
467+
2.5 ∂/∂t[y(t, ξ)](0, ξ) + y(0, ξ) - y(2.5, ξ) = 0, ∀ ξ ~ Uniform
467468
2.5 ∂/∂t[y(t, ξ)](2.5, ξ) + y(2.5, ξ) - y(5, ξ) = 0, ∀ ξ ~ Uniform
468469
2.5 ∂/∂t[y(t, ξ)](5, ξ) + y(5, ξ) - y(7.5, ξ) = 0, ∀ ξ ~ Uniform
469470
2.5 ∂/∂t[y(t, ξ)](7.5, ξ) + y(7.5, ξ) - y(10, ξ) = 0, ∀ ξ ~ Uniform
470-
2.5 ∂/∂t[y(t, ξ)](0, ξ) + y(0, ξ) - y(2.5, ξ) = 0, ∀ ξ ~ Uniform
471471
```
472472
Note that we made sure `t` had supports first over which we could carry out the
473473
evaluation, otherwise an error would have been thrown. Moreover, once the
@@ -483,9 +483,9 @@ julia> evaluate_all_derivatives!(model)
483483
484484
julia> derivative_constraints(dydt2)
485485
3-element Vector{InfOptConstraintRef}:
486+
6.25 dydt2(0, ξ) - y(0, ξ) + 2 y(2.5, ξ) - y(5, ξ) = 0, ∀ ξ ~ Uniform
486487
6.25 dydt2(2.5, ξ) - y(2.5, ξ) + 2 y(5, ξ) - y(7.5, ξ) = 0, ∀ ξ ~ Uniform
487488
6.25 dydt2(5, ξ) - y(5, ξ) + 2 y(7.5, ξ) - y(10, ξ) = 0, ∀ ξ ~ Uniform
488-
6.25 dydt2(0, ξ) - y(0, ξ) + 2 y(2.5, ξ) - y(5, ξ) = 0, ∀ ξ ~ Uniform
489489
```
490490

491491
Finally, we note that once derivative constraints have been added to the
@@ -495,10 +495,10 @@ and a warning will be thrown to indicate such:
495495
```jldoctest deriv_basic
496496
julia> derivative_constraints(d1)
497497
4-element Vector{InfOptConstraintRef}:
498+
2.5 ∂/∂t[y(t, ξ)](0, ξ) + y(0, ξ) - y(2.5, ξ) = 0, ∀ ξ ~ Uniform
498499
2.5 ∂/∂t[y(t, ξ)](2.5, ξ) + y(2.5, ξ) - y(5, ξ) = 0, ∀ ξ ~ Uniform
499500
2.5 ∂/∂t[y(t, ξ)](5, ξ) + y(5, ξ) - y(7.5, ξ) = 0, ∀ ξ ~ Uniform
500501
2.5 ∂/∂t[y(t, ξ)](7.5, ξ) + y(7.5, ξ) - y(10, ξ) = 0, ∀ ξ ~ Uniform
501-
2.5 ∂/∂t[y(t, ξ)](0, ξ) + y(0, ξ) - y(2.5, ξ) = 0, ∀ ξ ~ Uniform
502502
503503
julia> add_supports(t, 0.2)
504504
┌ Warning: Support/method changes will invalidate existing derivative evaluation constraints that have been added to the InfiniteModel. Thus, these are being deleted.

docs/src/manual/derivative.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,10 @@ evaluate_all_derivatives!
5555
has_derivative_constraints(::DerivativeRef)
5656
derivative_constraints(::DerivativeRef)
5757
delete_derivative_constraints(::DerivativeRef)
58+
InfiniteOpt.make_indexed_derivative_expr
59+
InfiniteOpt.derivative_expr_data
5860
evaluate_derivative
59-
allows_high_order_derivatives
61+
InfiniteOpt.allows_high_order_derivatives
6062
generative_support_info(::AbstractDerivativeMethod)
6163
support_label(::AbstractDerivativeMethod)
6264
InfiniteOpt.make_reduced_expr

src/InfiniteOpt.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,14 @@ end
6464
Base.@deprecate map_nlp_to_ast(f, expr) map_expression_to_ast(f, expr)
6565

6666
# Define additional stuff that should not be exported
67-
const _EXCLUDE_SYMBOLS = [Symbol(@__MODULE__), :eval, :include]
67+
const _EXCLUDE_SYMBOLS = [
68+
Symbol(@__MODULE__),
69+
:eval,
70+
:include,
71+
:derivative_expr_data,
72+
:make_indexed_derivative_expr,
73+
:allows_high_order_derivatives
74+
]
6875

6976
# Following JuMP, export everything that doesn't start with a _
7077
for sym in names(@__MODULE__, all = true)

0 commit comments

Comments
 (0)