-
Notifications
You must be signed in to change notification settings - Fork 6
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
IncompatibleBases
as a static versus dynamic check, i.e. should Basis
be isbits
?
#34
Comments
"Breaking change" can mean two different things in the Julia ecosystem. If it is breaking to some other libraries that rely on internal details of QuantumInterface, then that is not "technically/semver" breaking. If it is breaking the public API, then it is "technically/semver" breaking. I suspect much of what you are suggesting here is not "technically" breaking because it is about the internal layout of structs. We can still keep their old constructor API even after they are modified to be isbits. Can we do this in small increments? What is the smallest commit that can be made in this direction? Maybe making just one of the basis be isbits? Doing it in such small increments might be possible to do 90% of the work without any breaking. |
Yes I've been keeping the constructors the same as I've been experimenting on this, that's easy. The question is whether changes in parametric typing is considered breaking? This is also the same question about changing from |
I have not checked too carefully, but I suspect changes in the type parameters is not semver/technically breaking. Also, as we are going through these changes, especially through fixes, we should see whether we can simplify the dispatch signatures so that they are less susceptible to being broken by such internal changes. |
I have been thinking about related issues for a while and I would like to offer my two cents here. There is a place where relying on the type system for basis-equality checks is, in my opinion, problematic: CompositeBasis. A lot of functionality relies on type checking to exclude invalid basis combinations. This sometimes misses incompatibilities (as you describe above). It is also sometimes too strict (as in #27) and, in the case of CompositeBasis in particular, leads to a combinatorial explosion of possible basis types, because the concrete type of a CompositeBasis includes a tuple of the component basis: Of course, this is by design since we rely on types to test basis equivalence, but it can lead to excessive compilation overhead. Consider the basis This makes me consider whether we should rely much less on the type system for basis checking and have runtime (even runtime-dispatched) basis checking by default. We can choose to turn off these checks (e.g. via a global variable) for things like integration loops in time evolution. Interested to hear your thoughts @akirakyle and @Krastanov. One strategy might be to introduce runtime checks for CompositeBasis in particular (and maybe transition CompositeBasis to use a vector of component bases, rather than a tuple). |
I am also leaning towards using much more runtime tools. The incompatible basis checks are not needed in hot loops, so there is no engineering need to make them static. |
In terms of pure programming style I think the using the type system for basis checks is much more elegant. As for the performance cost (https://docs.julialang.org/en/v1/manual/performance-tips/#man-performance-value-type), I'm not sure I necessarily agree that the overhead is unmanageable since it really depends on the usage patterns? Ideally both approaches could be benchmarked, but that requires a bit of work. I think one thing to keep in mind is that the number of elements in a
I'm curious why you think a vector is preferable over a tuple? I would think that regardless of how checks are implemented, we should treat bases as immutable objects? |
The reason to use a vector is again to reduce "type load". The type of a tuple is specialized to the types of its entries, whereas a vector can have a generic element type. Any function that takes the tuple as an argument must be compiled again for each basis combination. Regarding your other point, it is quite possible to have say, 20-30 element composite bases. Say you only allow your local basis to either be FockBasis(1) or GenericBasis(2). That's still millions of possible combinations. Of course, we won't likely be hitting them all in practice, but it still seems wasteful to a new function for each. The compilation overhead can be significant because the bases pop up all over QO in type parameters. Any function that takes an operator or state argument specializes on the basis type, since the basis type is a type parameter of the state or operator, and hence needs to be recompiled for every new basis type encountered. Even might be worth removing the basis type parameters on operator and state types for this reason. |
Here is an example where a dynamic setup would be faster than the static type check:
If If we use more dynamism in the basis checks, then This issue has been coming up often in circuit simulations with QuantumClifford, and is something I need from QuantumOptics, but I have not made it possible yet. |
Alright I think I'm convinced that at least the potential explosion in types with making bases abstract type Basis end
struct GenericBasis{N} <: Basis
GenericBasis(N) = new{N}()
end
struct CompositeBasis{T<:Tuple{Vararg{Basis}}} <: Basis
bases
CompositeBasis(x) = new{typeof(x)}(x)
end
bases(b::CompositeBasis) = b.bases
nsubsystems(b::Basis) = 1
nsubsystems(b::CompositeBasis) = length(bases(b))
function reduced(b::CompositeBasis, indices)
if length(indices)==0
throw(ArgumentError("At least one subsystem must be specified in reduced."))
elseif length(indices)==1
return bases(b)[indices[1]]
else
return CompositeBasis(bases(b)[indices])
end
end
function ptrace(b::CompositeBasis, indices)
J = [i for i in 1:nsubsystems(b) if i ∉ indices]
length(J) > 0 || throw(ArgumentError("Tracing over all indices is not allowed in ptrace."))
reduced(b, J)
end
function timeit(bs)
@time r1 = [ptrace(b, [i for i=1:nsubsystems(b) if i%3==0]) for b in bs];
@time r2 = [ptrace(b, [i for i=1:nsubsystems(b) if i%3==0]) for b in bs];
return r1,r2
end
timeit([CompositeBasis(Tuple(GenericBasis(i+j) for j=1:40)) for i=1:100]); giving
If instead I replace the structs with struct GenericBasis <: Basis
N
end
struct CompositeBasis <: Basis
bases
end and run the benchmarks as
I get
So I think just by not parametrically typing subtypes of |
This is a really neat set of benchmarks and experiments, thank you! You are right about all the worries about mutation and generally the fact that runtime checks are less elegant. This is a frequent seen tradeoff between perfection and practicality. Julia usually lets people mutate private fields on the principle "we are all adults here". As long as these fields are not exposed as a public part of the API, I think the risk level of someone messing them up and mutating them is acceptable. |
Got it, vectors it is then 😅 I think this further means that for While this change will require a fair bit of updating in QuantumOpticsBase because there's a lot of methods which use the
As far as naming go, I personally find it quite confusing that the
Thoughts? |
The deeper I dig into the basis system as part of #32, the more I seem to see reasons for an overhaul 😅. This is of course very related to #27 and #33 but different enough that I thought it helpful to have a dedicated issue. Currently it seems there's two methods at tension for checking whether bases are compatible: the
samebases
machinery versus the typesystem. This is easily illustrated with an examplethrows
ERROR: QuantumInterface.IncompatibleBases()
meanwhile
throws
ERROR: DimensionMismatch: a has size (2,), b has size (3,), mismatch at dim 1
The relevant part isn't so much the error messages themselves, we can easily (and probably should) make the latter example throw
IncompatibleBases
, but in how these two situations are caught due to using the type systemWhen differences in bases are encoded within their types as currently happens with
SpinBasis
when called created with anisbits
type, then this pattern catches incompatible bases. However, if incompatible bases are not evident at the type level, for example when usingBigInt
which hasisbits
false, then a dynamicsamebases
check is required.I think making
Basis
isbits
could potentially to unlock performance benefits since this might allowStaticArrays
to be used! Then downside is that this means one has to carefully subtypeBasis
so that equality of bases is fully encoded in type parameters for whichisbits
is true. I think this is certainly doable for all the bases collected or proposed in #33. Of course this would also be a major breaking change.In either case, I think it makes sense to pick only one pattern, document it here, and try to stick to it in dependent packages.
The text was updated successfully, but these errors were encountered: