Qsymm workflow
Qsymm is a great tool, developed here in the group, but not everyone is very familiar with it. This is a Qsymm tutorial designed to change that. Qsymm already has extensive documentation and a few tutorials on read the docs, but this tutorial shows a start-to-finish workflow. It covers using the nonhermitian branch of qsymm to create real-space Hamiltonians, saving and loading models, how to test our models for various symmetries, and how to use our models in kwant systems.
import qsymm import numpy as np
In order to use the nonhermitian branch of qsymm, clone that branch into your environment, navigate into the directory and do pip install .
Making a real-space model for kwant to use
We want to make a hamiltonian matrix that we can supply to hopping and onsite functions when constructing our system in kwant. We want this matrix to obey certain symmetries that we're interested in.
This hamiltonian will be defined in real space, and will vary based on the bond length between sites d
rather than the wave vector k
.
Step 1: defining symmetries and generating a model
For nonhermitian models, we need to modify the actions of our symmetries accordingly.
- particle-hole and time-reversal symmetries send $H(k)$ to $-H^\ast(-k)$ or $H^\ast(-k)$ respectively. In real space, we write them such that we end up with $-H^\ast(k)$ and $H^\ast(k)$ respectively.
- we have to supply a modified hermiticity condition. Here reversing the bond means hopping in the opposite direction, so it is equal to the hermitian conjugate of the hopping. This translates to $H(-d)=H^+(d)$.
herm = qsymm.PointGroupElement( R = -np.eye(2), # 2 spatial dimensions conjugate = True, antisymmetry = False, U = np.eye(2), # unitary part of the symmetry transpose = True, #for hemitian conjugate, have to conjugate and transpose ) family = qsymm.continuum_hamiltonian( [herm], #the generating symmetries dim=2, #spatial dimension hermitian = False, #raises an error if you're not on the nonhermitian branch of qsymm total_power=1, #maximum power of k we want to appear in our model ) h_=qsymm.hamiltonian_generator.hamiltonian_from_family(family, tosympy=False) h_.tosympy()
$$ \left[\begin{matrix} 1.0 c_{2} + 1.0 i c_{4} k_{x} + 1.0 i c_{8} k_{y} & 1.0 i c_{0} - 1.0 c_{1} + 1.0 i c_{10} k_{y} - 1.0 c_{11} k_{y} + 1.0 i c_{6} k_{x} - 1.0 c_{7} k_{x} \\ - 1.0 i c_{0} - 1.0 c_{1} + 1.0 i c_{10} k_{y} + 1.0 c_{11} k_{y} + 1.0 i c_{6} k_{x} + 1.0 c_{7} k_{x} & - 1.0 c_{3} + 1.0 i c_{5} k_{x} + 1.0 i c_{9} k_{y} \end{matrix}\right] $$
We can check symmetries of our Hamiltonian model
M1 = qsymm.groups.mirror([0, 1]) M2 = qsymm.groups.mirror([1, 0]) TR = qsymm.time_reversal(2) PH = qsymm.particle_hole(2) C = qsymm.groups.chiral(2) gens = { M1, M2, C, TR, PH} candidates = qsymm.groups.generate_group(gens) sg, cg = qsymm.symmetries( h_, candidates=candidates, continuous_rotations=True, prettify=True, sparse_linalg=True ) sg, cg
([1], [])
No symmetries, since we didn't specify any. Let's now specify some.
When supplying symmetries to qsymm to build a model, we have to supply the unitary part. Part of it is easy to deduce, for example if the symmetry squares to -1, it will contain sigma_y
. Other than that, you have to ensure that the symmetry operation does what you expect it to in the basis you choose.
S = 2*qsymm.groups.spin_matrices(1/2) P_1 = qsymm.PointGroupElement(np.eye(2), conjugate=True, antisymmetry=True, U = S[0]) #particle-hole symmetry M_y = qsymm.PointGroupElement(np.diag([1,-1]), False, False, S[0]) #mirror symmetry in y rot = qsymm.ContinuousGroupGenerator(R = S[1], U = 0.5*S[2]) #continuous rotation symmetry family = qsymm.continuum_hamiltonian( [herm,P_1,M_y,rot], dim=2, hermitian = False, total_power=1, ) len(family)
0
No terms allowed it seems. This means either we have to go to 4x4, or that the unitary parts of our symmetries aren't consistent within the basis.
family = qsymm.continuum_hamiltonian( [herm, M_y, rot], dim=2, hermitian = False, total_power=1, ) h_=qsymm.hamiltonian_generator.hamiltonian_from_family(family, tosympy=False) sg, cg = qsymm.symmetries( h_, candidates=candidates, continuous_rotations=True, prettify=True, sparse_linalg=True ) sg, cg
([1, R(π), M([-1 0]), M([ 0 -1]), T, R(π) T, M([-1 0]) T, M([ 0 -1]) T], [exp(-i ϕ L)⋅H(k)⋅exp(i ϕ L) = H(R_ϕ⋅k) R_ϕ = R(ϕ) L = [[ 0.5+0.j 0. +0.j] [ 0. +0.j -0.5+0.j]] ])
You can also get additional symmetries you didn't intend in your model, like the time-reversal symmetry we see above, even though we didn't ask for it. You have to try out different unitary parts to get the result you want.
Once you have a hamiltonian, you make it into a model with:
model = qsymm.Model(h_, momenta=["k_x", "k_y"]) # sometimes the coefficients get freaky so we can round them. rounded_model = dict() for a,b in model.items(): rounded_model[a]=np.round(b,2) model = rounded_model qsymm.Model(model).tosympy()
Step 2: saving and loading a model
Here we show how we can move between a qsymm Model, a json serialization, then to a set of numpy arrays.
Encoding
import json from astropy.utils.misc import JsonCustomEncoder def pickle_this(model, name): '''model is a qsymm.Model ''' #name name = name + '.json' model_dict = { str(key) : model[key].tolist() for key in model.keys() } with open(name, 'w') as fp: json.dump(model_dict, fp, cls = JsonCustomEncoder)
Decoding
To load it, you have to use a custom decoder, that's not part of a package.
class JsonCustomDecoder(json.JSONDecoder): """ A decoder function tailored specifically to our purposes: take a dictionary of strings, output a dictionary of symbolic keys (Mul types etc.) and arrays. """ def __init__(self, *args, **kwargs): json.JSONDecoder.__init__(self, object_hook=self.object_hook, *args, **kwargs) def object_hook(self, obj): new_dict = dict() for key in obj.keys(): # have to back-convert the key from string to symbols symbolic = [] split_key = key.split("*") if str() in set(split_key): # there's a power: proceed with caution. This procedure is also valid for # only one single power being present. power_location = np.where(np.array(split_key) == str())[0][0] split_key_power = [ split_key[power_location - 1], split_key[power_location + 1], ] key_1 = split_key[: power_location - 1] key_2 = split_key[power_location + 2 :] split_key = key_1 + key_2 # key terms without the power # split key power is of the form ['k', 'x' ] symbolic.append( sympy.Pow(Symbol(split_key_power[0]), int(split_key_power[1])) ) # now add the rest for term in split_key: symbolic = [sympy.Mul(symbolic[-1], Symbol(term))] else: # no power, proceed normally for term in split_key: if symbolic == []: symbolic = [Symbol(term)] else: symbolic = [sympy.Mul(symbolic[-1], Symbol(term))] symbolic_key = symbolic[0] array = [] for line in obj[key]: lineski = [] for entry in line: lineski.append(entry[0] + 1j * entry[1]) array.append(lineski) new_dict[symbolic_key] = np.array(array) return new_dict
To unpickle, do something like
with open('filename.json', 'r') as fp: unpickled_dictionary = json.load(fp, cls = JsonCustomDecoder)
Step 3: using our model as a value function in a kwant system
While building a kwant system, you do something like
syst[lat.shape(shape_edge, (0, 0))] = onsite syst[lat.neighbors(n=1)] = hopping
for example.
onsite()
and hopping()
and value functions that take each site and each pair of sites connected by a bond in the system respectively, and return either an onsite or a hopping value for this site or bond.
We can use load our models into the function that builds our kwant system before calling onsite and hopping, and define the latter as:
def onsite( site, omodel, model_parameters **kwargs ): """ Value function used in the creation of the kwant FiniteSystem instances. Determines the onsite values. :param Site site: A kwant Site instance :param Model omodel: A qsymm lambdified Model :param dict omodel_parameters: the parameters to be used by omodel :rtype: valueFunction """ k_and_r = dict(k_x=0, k_y=0, r=0, os=1) return omodel(site, **omodel_parameters, **k_and_r) def hopping( site1, site2, hmodel, hmodel_paramters, **kwargs, ): """ Value function used in the creation of the kwant FiniteSystem instances. Determines the hopping values based on the distance between the two sites to be connected. Periodicity determines how distances are treated. :param Site site1,2: kwant Site instances :param Model hmodel: A qsymm lambdified Model :param dict hmodel_parameters: the parameters to be used by hmodel :rtype: valueFunction """ #extracting the bond length delta = site2.pos - site1.pos if la.norm(delta) != 0: d = delta / la.norm(delta) else: d = delta k_and_r = dict(k_x=d[0], k_y=d[1], r=1, os=0) # set onsite to 0 return hmodel(site1, site2, **hmodel_parameters, **k_and_r)
You see that we have to be able to turn off the onsite part of the model when we return the hopping value function, and vice versa for the onsite. We can do this by either adding additional coefficients to our models like so:
onsite_coeffs = ['mu1','lam1','lam2','mu2',0,0,0,0] hopping_coeffs = [0,0,0,0,'t1','o1','o2','t2'] h_os=qsymm.hamiltonian_generator.hamiltonian_from_family( family, coeffs = onsite_coeffs, tosympy=False ) h_hop=qsymm.hamiltonian_generator.hamiltonian_from_family( family, coeffs = hopping_coeffs, tosympy=False ) h_=h_os+h_hop
Or by saving the two different models h_os
and h_hop
separately.
You also notice that we provide "lambdified" models to kwant. What this means is that we are preparing the model to behave like a value function. It's important to note that this lambdification takes a bit of time, which adds up over all the sites and bonds of a lattice, so best to do it once at the beginning.
You'll also notice that onsite()
gets omodel
and hopping gets hmodel
. These aren't two distinct qsymm models, they're just the same qsymm model lambdified two different ways:
with open("filename.json", "r") as fp: model = json.load(fp, cls=JsonCustomDecoder) model = qsymm.Model(model, momenta=["k_x", "k_y"]) omodel = model.lambdify(onsite=True) hmodel = model.lambdify(hopping=True)
And there you have it.