Add DefaultState and DefaultStoppingCriterionState implementations - #32
Add DefaultState and DefaultStoppingCriterionState implementations#32lkdvos wants to merge 2 commits into
DefaultState and DefaultStoppingCriterionState implementations#32Conversation
Downstream packages had to write the same three things before running a single iteration: a state struct declaring `iterate`, `iteration` and `stopping_criterion_state`, an `initialize_state` allocating it, and an `initialize_state!` resetting it. This repository showed the cost itself, with `DummyState`, `NewtonState` and `HeronState` being the same struct three times over, each with a different field order. Add `DefaultState`, holding those three expected properties next to a single generic `data` field for anything else an algorithm carries from one step to the next. `data` is opaque: nothing is forwarded to it and none of its contents are exposed as properties, so access stays type stable and the container is the algorithm's choice. Give `DefaultStoppingCriterionState` the same `data` field, defaulting to `nothing` so existing zero-argument construction is unaffected. Add defaults for `initialize_state` and `initialize_state!` at both levels, taking their arguments positionally as `(problem, algorithm, iterate, state_data, stopping_state_data)` next to the keyword form the interface documents. An algorithm that is served by these now only has to provide a `Problem`, an `Algorithm` and a `step!`; since `step!` dispatches on the `Algorithm`, sharing one state type across algorithms costs no flexibility. `StopAfterIteration` no longer carries its own initialization pair, which the criterion-level defaults now cover. Rework the documentation so all three pages lead with the default and present a state of your own as the option, keeping `HeronState` as the closing example on the interface page. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
@mtfishman, @jack-dunham - linking you both here in case you would like to have a look/share opinions |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #32 +/- ##
==========================================
+ Coverage 99.60% 99.63% +0.02%
==========================================
Files 6 7 +1
Lines 255 274 +19
==========================================
+ Hits 254 273 +19
Misses 1 1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
I like the idea, though until now I mainly used a slightly different approach (in Manifolds.jl and Manopt.jl that is): the I am not saying my approach is per se better. I do see the danger of “overwrapping” in your case though. Somewhen you might end up with “DefaultData” and therein continue with some further default... What do you think? Another note, since we are discussing states, in Manopt.jl we introduced callbacks a while back, see JuliaManifolds/Manopt.jl#626 or https://manoptjl.org/stable/tutorials/HowToCallback/ for the new tutorial. Since we do talk about a default state, we could add that here as well, indeed. The field is just a dictionary of functions (of an agreed-upon-signature), the actual callback code is quite short https://github.com/JuliaManifolds/Manopt.jl/blob/master/src/base/state/callback.jl and the actually “hooks” (places where the callbacks are called) are also just one line each, see for example the generic ones at https://github.com/JuliaManifolds/Manopt.jl/blob/5e362ee0f07fb656dfdf1a40a616a94c3ba27260/src/solvers/solver.jl#L164-L177 |
I definitely feel like I agree with you here, I think this was what I was getting at with the question about replacing the abstract Let me try that out just to see how it would look, the only annoying part being that it is breaking but that's what 0.x versions are for I guess. |
|
I still think the |
|
Alright, second attempt: I took the What I like the most about this approach is that we no longer rely on correctly defining the right fields for compliance with this package, and in the meantime get to hide a lot of the boilerplate that is related to that. What I like a little less, although I think it might be acceptable, is that because we are defining the structs, it is a little less transparent what is going on and which data is put where. Ultimately, I think I quite like this approach. |
|
I like the idea of having a |
|
In the last commit the abstract |
|
This last commit was the idea to get rid of the abstract state, so the default path is without as much friction, and then have explicit implementations add a |
|
The state.iterate # returns the iterate (the data that gets passed around)
state.iteration # returns the iteration. Then one can define concrete |
|
I was maybe too much still in The “dynamic structure with names tuple” sounds a bit like a hack. For me it then depends a lot on how “safe” that is, whether I like that. I am not yet sure. If we always one have one state, then sure we do not need a default one. |
This PR is an attempt to reduce some boilerplate in downstream implementations, both surrounding
structdefinitions as well as theinitialize_stateboilerplate code.Before, we had to write the same three things before running a single iteration: a state struct declaring
iterate,iterationandstopping_criterion_state, aninitialize_stateallocating it, and aninitialize_state!resetting it.Since none of this is really specific to any algorithm, I've added
DefaultState, holding those three expected properties next to a single genericdatafield for anything else an algorithm carries from one step to the next.Similarly, I gave
DefaultStoppingCriterionStatethe samedatafield, defaulting tonothingso existing zero-argument construction is unaffected.Then, I've added defaults for
initialize_stateandinitialize_state!at both levels, taking their arguments positionally as(problem, algorithm, iterate, state_data, stopping_state_data)next to the keyword form the interface documents.We can discuss the keyword form or the positional form, I felt like keywords here might lead to awkward dispatch and type-stability issues, but I'm open to reconsider this.
While this is in principle breaking for the new recommended way of doing this, it should be completely backwards compatible.
As a result, an algorithm now only has to provide a
Problem, anAlgorithmand astep!.StopAfterIterationno longer carries its own initialization pair, which the criterion-level defaults now cover.I've also reworked the documentation so all three pages lead with the default and present a state of your own as the option, keeping
HeronStateas the closing example on the interface page.Something to discuss:
In principle, this approach is actually as expressive as the abstract
Statewith subtypes, since you can always add whatever you like in the.datafields, and can additionally dispatch on that. In that sense, it might actually make sense to do a (breaking) change here and simply makeDefaultState -> State, and remove the abstract type altogether. While this is breaking, it also enforces that the required properties are present, which seems like a good quality to have.As a result, we could consider marking
initialize_stateas public but non-exported, since the default implementation now actually should be able to "just work".As always, naming and design choices discussions more than welcome, this is just my attempt at simplifying some of the implementations and removing some of the boilerplate.