Closed danielwe closed 6 years ago
Let's remove the identifier
from the constructor of all LocalOperator
subclasses. We only keep the _identifier
class attribute, as a constant.
To restore the ability have different identifiers in different Hilbert spaces when printing out expressions, let's add a keyword argument local_identifiers
to the constructor of the LocalSpace
class. This could receive a dict that maps LocalOperator
subclass names to an identifier string. The call would be e.g.
hs = LocalSpace(1, dimension=10, local_identifiers={'Destroy': 'b', 'Squeeze': 'S'})
Then, when printing any LocalOperator
, we'd look at the local_identifiers
, with a fallback to the _identifier
class attribute of the operator.
Note that the keys in local_identifiers
need to be strings (not classes) to make sure that expressions can be serialized and compared correctly. Hilbert spaces that differ in the local_identifiers
must be distinguishable (LocalSpace(1, local_identifiers={'Destroy': 'b'}) != LocalSpace(1)
)
Actually, I think we might want to remove the Create
class, and replace it with a Create
function that just returns Destroy(...).dag()
. Otherwise, it's possible to end up with inconsistencies (you'd have to assign the same identifier to both 'Create'
and 'Destroy'
)
Radical idea: let local operators be attributes of a local space. Instantiate ls = LocalSpace(1)
, and there you have ls.destroy, ls.create
etc. (with identifiers as given in your suggestion). Of course, retain Destroy(ls)
etc. for backward compatibility, now as a function that returns ls.destroy
. Haven't thought this through, is there an obvious reason why it's a terrible idea?
It might only make sense if we also go through with what we discussed about speciation amongst local spaces, such that a local space either supports bosonic operators or spin operators, not both at the same time.
I don't think that'll work (if I correctly understand what you're proposing): We can't get around having the proper class structure for any algebraic object. It's possible of course to have convenience functions like ls.destroy()
that would be equivalent to Destroy(hs=ls)
, but not the other way around. Actually, the convenience function would probably be more something like ls.local_op('Destroy')
, because ls.create
is already the rule-applying constructor, and also because people might define additional local operators.
Yes, the operators have to be instances of classes. And you're right, we need specific classes for specific types of operators such as Create and Destroy to facilitate algebra, so forget about replacing Destroy
with a function.
I think the essence of what I'm proposing is to promote the Hilbert space algebra to play a more central role and make local operators subordinate to their respective spaces rather than the other way around. Currently, there is a single generic LocalSpace class on whose instances you can tack on arbitrary LocalOperators to sort-of define the physical system you're modeling. My idea is that a local degree of freedom should be an instance of a particular subclass of LocalSpace that defines a canonical basis for its own algebra of operators, and the only operators allowed to act on that space are algebraic combinations of these basis elements.
Consider the most elementary example, a single, continuous degree of freedom (the system that, given the right Hamiltonian, becomes a harmonic oscillator). Suppose we define it as class Boson(LocalSpace)
, an consider an instance cavity = Boson('label')
. The obvious operator basis is the Harmonic oscillator creation and annihilation operators, which could be accessed through something cavity.create
and cavity.destroy
(I realize that the create
name is taken, so we would have to come up with another name or solution). Now, trying to create a spin operator acting on this space, e.g., Jplus(hs=cavity)
, should fail, since the space does not contain the requisite degree of freedom. Meanwhile, Destroy(hs=cavity)
just returns cavity.destroy
, which is already an instance of the Destroy
class (of course, it doesn't matter if it formally instantiates a new object that is equal to cavity.destroy
, but it might as well return the same object). Any valid operator on cavity
can be expressed in terms of cavity.create
and cavity.destroy
, e.g., x = cavity.create + cavity.destroy
.
In other words, the operator algebra will still be defined by the operator class hierarchy as today, but local operators must be instantiated subordinate to a local space that explicitly restricts the kinds of operators that are valid on that space. The benefits are enforcing consistency and eliminating bugs like the one we're discussing, and perhaps also providing a safeguard against conceptual errors when building models (I don't have a specific example of this at the moment). I also think it provides a clearer mental model of local spaces and operators.
As always, composite systems are created by taking tensor products of elementary local spaces. Currently, the hierarchy below LocalOperator only supports three kinds of systems, bosonic systems, two-level systems, and spin systems, so it's not a big deal to write the corresponding subclasses of LocalSpace required to build any system supported today.
Does this make sense? Not saying that this is something we should definitely do, but would like to hear your thoughts and why it might be a bad idea.
Everything you say makes total sense. We should definitely create subclasses of LocalSpace
, and associate specific operators with those subclasses. The way I see this being implemented is to say e.g. Jz(hs=SpinSpace(0, spin=2))
. Instantiating Jz
with something other than a SpinSpace
would raise a TypeError
. I think this is sufficient to make the operators "subordinate" to the correct space. I'm not sure I see a good way to instantiate operators from the Hilbert space directly: this would require to somehow register the operator classes with the Hilbert space (as we'd expect users to define their own custom operators).
Conceptually, our Hilbert spaces are already defined through their associated basis, which is implemented as a list of string labels. If no basis is defined, the basis states are only referenced through their integer index, as a fallback that still allows a number of algebraic manipulations. Instantiating the a LocalSpace
without a dimension/basis, as e.g. just LocalSpace(0)
is generally not the preferred approach. Especially when using QNET to drive a numerical simulation (converting to qutip), all Hilbert spaces have to have a well-defined basis (you can substitute a Hilbert space with a basis for on without a basis to achieve this). With an explicit dimension/basis, since all operators are ultimately expressed as ket-bras on that basis, when instantiating a LocalSpace
, we thus already define the canonical basis and the possible operators. When we refine the LocalSpace
into subclasses, all we do is place restrictions on the acceptable dimension and the basis labels.
In more detail I would propose the following:
Create the following subclasses of LocalSpace
: FockSpace
, SpinSpace
, TLSSpace
(names are up for debate)
a. FockSpace
has no particular restrictions compared to the current LocalSpace
.
b. SpinSpace
can be instantiated with a parameter "spin" (int
or sympy.Rational
; abbrev "s") instead of "dimension" which implies a dimension of 2s+1. If "spin" is given without an explicit list of basis labels, the default labels are "-{s}" ... "0" ... "+{s}". It would also be possible to use the labels "{s}, -{s}", "{s}, -{s+1}", ..., although this risks confusion with a ProductSpace
c. TLSSpace
has a fixed dimension of 2 (default labels "0", "1")
Create matching subclasses of LocalOperator
: FockOperator
, SpinOperator
, TLSOperator
. All these do is check that they are instantiated with the correct HilbertSpace
.
The current Displace
, Phase
, Squeeze
, Create
, and Destroy
classes become subclasses of FockOperator
. Jz
, Jminus
, and Jplus
become subclasses of SpinOperator
. We might consider adding the Pauli operators as subclasses of TLSOperators
(instead of a wrapper function expressing them as LocalSigma
right now). We can delay the definition of TLSSpace
until we actually need it. LocalSigma
remains a direct subclass of LocalOperator
, as it is conceptually the most general operator (In fact any operator can be written as a sum of LocalSigma
)
For Hilbert spaces which have no specified dimension or an explicit basis, integer indexes for the basis states can occur in the following places:
j
, k
in the LocalSigma
/LocalProjector
constructorindex_j
, index_k
property of LocalSigma
m
of Jpjmcoeff
, Jzjmcoeff
, Jmjmcoeff
(if shift=True
)HilbertSpace.basis_state
LocalSpace.next_basis_or_label
label_or_index
in the BasisKet
constructorFor example, BasisKet(0, hs=0)
, BasisKet("0", hs=LocalSpace(0, dimension=2)
and BasisKet(0, hs=LocalSpace('tls', basis=("g", "e")))
all refer to the first basis state of the Hilbert space, BasisKet("0", hs=LocalSpace(0, dimension=2))
and BasisKet("g", hs=LocalSpace('tls', basis=("g", "e")))
select a basis state by label. Note that BasisKet("0", hs=0)
is invalid, as no basis has been set (or dimension, which would automatically generate a basis with labels "0", "1", ...)
For a FockSpace
and TLSSpace
, the zero-based indexing of basis states is canonical and will not lead to any surprises. For the SpinSpace
, there is a possible source of confusion for integer-values spins where e.g. for spin=2 the canonical labels would be "-2", "-1", "0", "1", "2": A user might accidentally say BasisKet(0, hs=SpinSpace(0, s=2))
with the intention to get BasisKet("0", hs=SpinSpace(0, s=2))
, but would instead get BasisKet("-2", hs=SpinSpace(0, s=2))
, due to the zero-based indexing.
I was briefly considering to include an index offset in the SpinSpace
class that would automatically shift indices such that the routines listed above when an integer index instead of a label is given. However, after some thought I think this is neither possible nor a good idea:
SpinSpaces
as it is to get the "wrong" state by giving an integer index instead of a string label.Therefore, I think we should not change the semantics of the indexing at this point.
Does all of the above sound like a good plan?
After discussion with @danielwe, a few more comments:
I was overstating the need to define a basis. Mathematically, any FockSpace
is typically infinite-dimensional and we wouldn't instantiate it with a dimension
or basis
. Only when we want to do numerics we have to move to a truncated FockSpace
.
The documentation of LocalSigma
should make it clear that is has nothing to do with two-level-systems, but is QNET's general way of saying |j><k|
on any Hilbert space
For convenience, the SpinSpace
should also accept a string for spin
which gets sympyfied:
SpinSpace(0, spin='1/2')
is equivalent to
SpinSpace(0, spin=sympy.sympify('1/2'))
It is better to not define a TLSSpace
. The definition of the Pauli-Matrices as wrapper function expressing the operator in terms of LocalSigma
should be kept. This allows to define a TLS from two different perspectives. Either as a FockSpace
truncated to two levels (labels "0", "1"), or as SpinSpace(spin='1/2')
(labels "-1/2", "1/2"). Different people prefer different conventions, and we can handle both at the same time. There should be a section in the documentation about TLS that talks about this explicitly.
After trying this out in practice, it turns out having FockSpace
adds some unnecessary complications. Most importantly, it makes the creation of implicit Hilbert spaces (e.g. LocalSigma(0, 1, hs=1)
more complicated. Also, for at least some of the "FockOperators", e.g. Phase
, it's perfectly sensible to apply them to a spin state. Therefore, leaving LocalSpace
as a generalized Fock space, and only having an additional SpinSpace
that's a subclass of LocalSpace
works better.
Identifiers on LocalOperators are not preserved by simplification and substitution rules.
Tying identifiers to particular instantiations of local operators might not be the best idea. The identifier should rather be a property of the corresponding local Hilbert space factor.