-
Notifications
You must be signed in to change notification settings - Fork 23
Changes to the iterator interface #255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: network_solvers
Are you sure you want to change the base?
Conversation
RegionPlan is ommited as this is just vector of kwargs whos type is unimportant
…stract type Other changes: - Both `sweep_callback` and `region_callback` in `sweep_solve` now take only one positional argument, the sweep iterator. - Iterating `SweepIterator` now automatically performs the RegionIteration - Added an 'adapter' `PauseAfterIncrement` that allows `SweepIterator` to be iterated without performing region iteration - `RegionIteration` now tracks the current sweep number - Replaced some function calls with explict calls to constructors to make it clear when new iterators are being constructed (instead of returned from a field etc). Note, AbstractNetworkIterator interface requires some documentation.
|
…me but returning a tuple (region, kwargs) at each step
src/solvers/iterators.jl
Outdated
region_iter::RegionIterator{Problem} | ||
which_sweep::Int | ||
function SweepIterator(problem, sweep_kws) | ||
sweep_kws = Iterators.Stateful(sweep_kws) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sweep_kws = Iterators.Stateful(sweep_kws) | |
stateful_sweep_kws = Iterators.Stateful(sweep_kws) |
I would find that a bit clearer (i.e. it is easier to keep track that we are now using the stateful version of sweep_kws
).
src/solvers/iterators.jl
Outdated
# | ||
|
||
mutable struct SweepIterator{Problem} <: AbstractNetworkIterator | ||
sweep_kws |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we parametrize SweepIterator
by the type of sweep_kws
? Or are we expecting it could be modified such that it changes type (though if that is the case we could make it explicit by setting the type parameter to Any
)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering what would be the advantage of having this parameterized? I think the power of this SweepIterator
object comes from that fact that it can be basically any object implementing the iterate
interface (for example, an iterator that loads the kwargs from a file, even). The eltype
of the iterator should be a NamedTuple
, but then we cannot define this type necessarily as different kwargs then lead to different concrete NamedTuple
types.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Purely for the sake of type stability, in cases where that is possible. I was just making the point that by parametrizing and then setting the default to Any
we could allow for either option but allow it to be dynamic by default, but we could add that option later if needed.
Also I would recommend ordering the fields in the struct as region_iter
, sweep_kws
, which_sweep
, that seems more natural to me since we are inputting the problem
argument as the first argument and generally dispatching on the problem
.
src/solvers/sweep_solve.jl
Outdated
# Don't compute the region iteration automatically as we wish to insert a callback. | ||
for _ in PauseAfterIncrement(sweep_iterator) | ||
for _ in region_iterator(sweep_iterator) | ||
region_callback(sweep_iterator; outputlevel=outputlevel) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Passing outputlevel
is a bit funny to me (I think it is there for historical reasons). I would think that it would be easy enough to pass keyword arguments as part of the callback, so I think it can be removed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Otherwise, we could have users pass sweep_callback_kwargs
and region_callback_kwargs
NamedTuples.
So basically the alternatives would be:
sweep_solve(sweep_iterator; region_callback=(x -> default_region_callback(x; outputlevel=1))
or:
sweep_solve(sweep_iterator; region_callback_kwargs=(; outputlevel=1))
I'm ok with either one, though I'd bias towards the first one for now since we can always add the second one later, for the sake of keeping this level of the code simpler.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't agree with passing outputlevel
as a kwarg, I agree its a bit weird, and I left it in for historical reasons as you mention.
IMO, options like outputlevel
can be defined as part of a closure in a wrapper function and then exposed to a user, if we (or another user) wishes to do that. We should not assume a notion of outputlevel
for all callbacks.
src/solvers/sweep_solve.jl
Outdated
# I suspect that `sweep_callback` is the more commonly used callback, so allow this to | ||
# be set using the `do` syntax. | ||
function sweep_solve(sweep_callback, sweep_iterator; kwargs...) | ||
return sweep_solve(sweep_iterator; sweep_callback=sweep_callback, kwargs...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return sweep_solve(sweep_iterator; sweep_callback=sweep_callback, kwargs...) | |
return sweep_solve(sweep_iterator; sweep_callback, kwargs...) |
function default_region_callback(sweep_iterator; kwargs...) | ||
return sweep_iterator | ||
end | ||
|
||
function default_sweep_callback(sweep_iterator; kwargs...) | ||
return sweep_iterator | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's better to remove the kwargs...
to make sure we don't accidentally pass unsupported keyword arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was again an artifact of supporting outputlevel
for the already written default callbacks. I agree it should not be supported.
test/solvers/test_applyexp.jl
Outdated
function collect_times(problem; kws...) | ||
push!(times, ITensorNetworks.current_time(problem)) | ||
function collect_times(sweep_iterator; kws...) | ||
push!(times, ITensorNetworks.current_time(ITensorNetworks.problem(sweep_iterator))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe it's better to use field access here, i.e. sweep_iterator.problem
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah SweepIterator
has no field problem
. RegionIterator
is the object with the field problem
. Using a field getter means we can obtain the Problem
object the same way for both SweepIterator
and RegionIterator
.
src/solvers/adapters.jl
Outdated
done(NC::PauseAfterIncrement) = done(NC.parent) | ||
state(NC::PauseAfterIncrement) = state(NC.parent) | ||
increment!(NC::PauseAfterIncrement) = increment!(NC.parent) | ||
compute!(NC::PauseAfterIncrement) = NC | ||
|
||
PauseAfterIncrement(NC::PauseAfterIncrement) = NC |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a small recommendation, I would prefer a variable name besides NC
, maybe we could just use iterator
.
… true during the iteration
…`prev`/`next` naming convention
src/solvers/iterators.jl
Outdated
function increment!(SR::SweepIterator) | ||
SR.which_sweep += 1 | ||
sweep_kwargs, _ = Iterators.peel(SR.sweep_kws) | ||
SR.region_iter = RegionIterator(problem(SR); sweep=state(SR), sweep_kwargs...) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Related to our discussion around how the RegionIterator
gets updated and how the problem
gets passed around, maybe it could help to wrap this constructor call into a function, for example:
function update_region_iterator(iterator::RegionIterator; kwargs...)
return RegionIterator(iterator.problem; kwargs...)
end
or something like that (mostly to hide the detail of how the problem
gets passed along at this level of the code).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, what does SR
stand for? Generally I prefer lower case variable names, and also more descriptive ones, say sweep_iter
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think SR
was an abbreviation of SweepRegionIterator
which was a previous name for something I was using previously...
On the first point, I feel like update_region_iterator
is not really descriptive of what the function as implemented there does. It does not update an existing RegionIterator
, it creates a new one via the constructor. I do like however the following:
function update_region_iterator!(iterator::SweepIterator; kwargs...)
iterator.region_iter = new_region_iterator(iterator.region_iter; kwargs...)
return iterator
end
where new_region_iterator
is a renaming of the method you describe. Let me know if you agree.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not update an existing
RegionIterator
, it creates a new one via the constructor. I do like however the following:
I suppose that's a matter of perspective, i.e. we could think of it as a fancy kind of "setter" method that keeps some properties and sets other ones, which for immutable types has to create a new object by definition, but I'm ok with new_region_iterator
.
To be simplified...
This removes the function `region_step`. Default kwargs for these functions (none currently) are now splatted in using the `default_kwargs` function. Remove `sweep` kwargs as this can be obtained from the `RegionIterator`.
Includes the code change `(region, kwargs)` to `region => kwargs` for readability, but I also think the `Pair` data structure is more appropriate here. Reversing the region now happens in seperate function.
These are no longer necessary.
… kwargs get passed to functions
This is so region_iter (the mutating arg) appears first in the function sig
- Reordered the struct fields to be consistant with each other - Some field and function renames
Some comments: Bearing in mind the discussions about avoiding the proliferation of methods that don't do a lot (i.e. just pass data from structs as arguments), I have elected to take the opposite approach regarding the interface defined for If one needs some of the runtime data from For that reason, I think supporting both makes sense. One can define a method in two ways: default_kwargs(f, ::MyProblem)) # value domain
default_kwargs(f, ::Type{<:MyProblem)) # type domain If no method in the value domain is set, then it falls back to the one in the type domain. I have (briefly) described the interface in the docstring of Some more things to discuss:
On the last point, we should be strict about asking users to only define |
That sounds reasonable to me. |
src/solvers/adapters.jl
Outdated
# Essential definitions | ||
Base.length(adapter::EachRegion) = length(adapter.parent) | ||
state(adapter::EachRegion) = state(adapter.parent) | ||
increment!(adapter::EachRegion) = state(adapter.parent) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix:
increment!(adapter::EachRegion) = state(adapter.parent) | |
increment!(adapter::EachRegion) = increment!(adapter.parent) |
But this should really flatten nested iterator into an iterator over all regions.
src/solvers/default_kwargs.jl
Outdated
default values set in the function signature. | ||
""" | ||
default_kwargs(f) = default_kwargs(f, Any) | ||
default_kwargs(f, obj) = _default_kwargs_fallback(f, obj) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Marry this with function signatures.
src/solvers/default_kwargs.jl
Outdated
Return the keyword arguments to be passed to the function `f` for the current region | ||
defined by the stateful iterator `iter`. | ||
""" | ||
function current_kwargs(f::Function, iter::RegionIterator) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have this splat kwargs into default_kwargs
.
src/solvers/subspace/subspace.jl
Outdated
|
||
return expand_maxdim | ||
end | ||
function default_kwargs(::typeof(compute_expansion), iter::RegionIterator) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be removed.
src/solvers/adapters.jl
Outdated
the loop body in place of `compute!`. | ||
""" | ||
struct PauseAfterIncrement{S<:AbstractNetworkIterator} <: AbstractNetworkIterator | ||
struct NoComputeStep{S<:AbstractNetworkIterator} <: AbstractNetworkIterator |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
struct NoComputeStep{S<:AbstractNetworkIterator} <: AbstractNetworkIterator | |
struct JustIncrement{S<:AbstractNetworkIterator} <: AbstractNetworkIterator |
…rs into a single iterators over the regions. The length of the iterator is then nsweeps * nregions.
…s of the associated function. - This comes at the cost of some verbosity regarding getting and splatting the kwargs from the region iterator, but the usefulness of `default_kwargs` is now much wider and also more well defined - Introduce macro `@default_kwargs` for doing this automatically.
I think this is clearer than `JustIncrement` which might not be clear to non-native English speakers (maybe), and avoids the case of `OnlyIncrement` being confused with "the only increment".
The Because of this, getting and splatting kwargs from the func_kwargs = region_kwargs(func, iter) and then do something like func(arg1, iter, arg2; default_kwargs(func, arg1, iter, arg2; func_kwargs)...) to do the default overwriting. The reason for this is that This system is now more or less independent of the solvers code, but perhaps it is best to wait for the rewrite before considering if it's useful across the board of not. I just need the fix the broken tests in @with_defaults func(arg1, iter, arg2; func_kwargs...)
# expands to
func(arg1, iter, arg2; default_kwargs(func, arg1, iter, arg2; func_kwargs)...) |
This PR attempts to define an interface for the iteration pattern that is similar to that of DifferentialEquations (as described in the Google doc), however in this case it was not as simple. I have described why and what I did below:
AbstractNetworkIterator
The
iterate
function in Julia really describes how one should transition between states, i.e. from A to B.For example, when iterating through
SweepIterator
, one must define the newRegionIterator
for the next sweep. This is something that is suitable to be wrapped initerate
as it essentially is the step required to transition from one sweep to the next. What is less obvious is when to perform the region iteration itself such that the codeperforms the actual calculation. The region iteration is not really something that is part of "transitioning from sweep A to B" but rather work that is performed during a given sweep. This makes it somewhat difficult to reconcile the state of the data (such as the tensors themselves etc) with the state of the iterator.
Considering what iteration lowers to:
One has computed and iterated before any callback function. Each callback essentially acts before each computation, which is perhaps little awkward, but not necessarily a problem. What is a problem is that the first step is an exception to this; we don't get to execute a callback at step 1 before any computation happens.
To avoid this problem, and allow the callback to execute after the computation I have decide to separate out the "transition" step and the computation step into two functions
increment!
andcompute!
respectively. This also makes it clear which code is responsible for moving the iterator from A to B, and which code is responsible for performing computation while in state A (or B etc). We now also make the first call toincrement!
implicit. That is, the iterator should, when initialized, run the necessary code such that it is ready for computation at the first step. In a way, this is as if we have transition from some abstract 0th state to the 1st state. In the first call to iterate only the call toincrement!
is then skipped.This is the specific interface of
AbstractNetworkIterator
. AnAbstractNetworkIterator
is a stateful iterator and can be summarized by the followingiterate
definition:The function
done
just checks that the iteration is complete. Note, we increment before we compute, therefore we requirewith a
>=
rather than>
to avoid over shooting by 1. Simple subtypes ofAbstractNetworkIterator
can use the above method by definingstate
andBase.length
, however one can instead choose to dispatch ondone
itself for more complicated cases (seeSweepIterator
for an example).The code
when
iter::AbstractNetworkIterator
now executes the callback function after the computation step, but before the work has been done to transition to the next state.More details
compute!
function be default just returns the iterator itself (and does nothing else). One can choose to return something else to be used in the body of a loop if desired.increment!
has no default implementation on purposePauseAfterIncrement
adapter iterator as one example of how the interface can be used. This is a general version of the proposed adapter that would allow manual iteration over the region.Other changes
Misc
SweepIterator
now has a single type parameterProblem
that can be used for dispatch (useful for default callbacks, see below).RegionIterator
now has an additional field,sweep
that is constant and simply stores the current sweep the region iteration is part of. This is to avoid constantly passing around thissweep
keyword when dispensing the sweep iterator in favor of the simpler region iterator.sweep_solve
sweep_solve
is now just a wrapper over the iterators that allows callbacks to be passed (and therefore requiresPauseAfterIncrement
).sweep_iterator
as the only positional argument.kwargs
for now.sweep_callback
andregion_callback
functions have hasdefault_
prepended to their function names to distinguish them from the keyword argument (and to make their purpose clear).Notes