.. _tutorial_basics:

Qsymm Basics
============


.. seealso::
    The complete source code of this example can be found in :jupyter-download-script:`basics`.
    A Jupyter notebook can be found in :jupyter-download-notebook:`basics`.

.. jupyter-kernel::
    :id: basics

Getting started with Qsymm is as simple as importing it:

.. jupyter-execute::

    import qsymm

To make effective use of Qsymm we'll also need a few other utilities: ``numpy``
handling numeric arrays, and ``sympy`` for symbolic mathematics:

.. jupyter-execute::

    import numpy as np
    import sympy

In all the following tutorials we will use these standard imports, and they won't
be explicitly shown


Defining a Qsymm model
----------------------

Let's start by defining a 3D Rashba Hamiltonian symbolically as a Python string:

.. jupyter-execute::

    ham = ("hbar^2 / (2 * m) * (k_x**2 + k_y**2 + k_z**2) * eye(2) +" +
            "alpha * sigma_x * k_x + alpha * sigma_y * k_y + alpha * sigma_z * k_z")

We can then create a Qsymm `~qsymm.model.Model` directly from this symbolic Hamiltonian:

.. jupyter-execute::

    H = qsymm.Model(ham)

We can then directly inspect the contents by printing the `~qsymm.model.Model`:

.. jupyter-execute::

    print(H)

We can also extract a more readable representation by using the ``tosympy`` method, which
converts the `~qsymm.model.Model` to a ``sympy`` expression:

.. jupyter-execute::

    H.tosympy(nsimplify=True)

The argument ``nsimplify=True`` makes the output more readable by forcing ``sympy`` to elide
factors of ``1.0`` that multiply each term. Note that Qsymm automatically interprets the symbols
``sigma_x``, ``sigma_y`` and ``sigma_z`` as the Pauli matrices, and ``eye(2)`` as the 2x2
identity matrix.

`~qsymm.model.Model` as a ``momenta`` attribute that specifies which symbols are considered the
momentum variables:

.. jupyter-execute::

    H.momenta

By default Qsymm assumes that your model is written in 3D (even if it does not include all 3
momenta). To define a lower-dimensional model you must explicitly specify the momentum
variables, e.g:

.. jupyter-execute::

    ham2D = ("hbar^2 / (2 * m) * (k_x**2 + k_z**2) * eye(2) +" +
             "alpha * sigma_x * k_x + alpha * sigma_y * k_z")
    H2D = qsymm.Model(ham2D, momenta=['k_x', 'k_z'])

.. jupyter-execute::

    H2D.tosympy(nsimplify=True)

.. jupyter-execute::

    H2D.momenta


Defining group elements
-----------------------
Qsymm is all about finding and generating symmetries of models, so it is unsurprising
that it contains utilities for defining group elements.

Below are a few examples of the sorts of things you can define with Qsymm:

.. jupyter-execute::

    # Identity in 3D
    E = qsymm.identity(3)
    # Inversion in 3D
    I = qsymm.inversion(3)
    # 4-fold rotation around the x-axis
    C4 = qsymm.rotation(1/4, [1, 0, 0])
    # 3-fold rotation around the [1, 1, 1] axis
    C3 = qsymm.rotation(1/3, [1, 1, 1])
    # Time reversal
    TR = qsymm.time_reversal(3)
    # Particle-hole
    PH = qsymm.particle_hole(3)

The documentation page of the `qsymm.groups` module contains an exhaustive list
of what can be generated.

Alternatively, to define a point group element we initialize it directly by providing its real and orbital space action:

.. jupyter-execute::

    qsymm.PointGroupElement(R=np.diag([1, -1]), U='kron(sigma_y, sigma_z)')

As with other Qsymm objects we can get a readable representation of these
group elements:

.. jupyter-execute::

    C4

.. jupyter-execute::

    TR

Given a set of group generators we can also generate a group:

.. jupyter-execute::

    cubic_gens = {I, C4, C3, TR, PH}
    cubic_group = qsymm.groups.generate_group(cubic_gens)
    print(len(cubic_group))

Group elements can be multiplied and inverted, as we would expect:

.. jupyter-execute::

    C3 * C4

.. jupyter-execute::

    C3**-1

We can also apply group elements to the `~qsymm.model.Model` that we defined
in the previous section:

.. jupyter-execute::

    H_with_TR = TR.apply(H)
    H_with_TR.tosympy(nsimplify=True)


Defining continuous group generators
------------------------------------
In addition to the group elements we can also define generators of continuous groups
using `qsymm.groups.ContinuousGroupGenerator`:

.. jupyter-execute::

    sz = qsymm.ContinuousGroupGenerator(None, np.array([[1, 0], [0, -1]]))

The first argument to `~qsymm.groups.ContinuousGroupGenerator` is the realspace rotation generator;
by specifying ``None`` we indicate that we want the rotation part to be zero. The second
argument is the unitary action of the generator on the Hilbert space as a Hermitian matrix.

Applying a `~qsymm.groups.ContinuousGroupGenerator` to a `~qsymm.model.Model` calculates the commutator:

.. jupyter-execute::

    sz.apply(H).tosympy(nsimplify=True)

For the 3D Rashba Hamiltonian we defined at the start of the tutorial spin-z is not conserved, hence
the commutator is non-zero.


Finding symmetries
------------------
The function `~qsymm.symmetry_finder.symmetries` allows us to find the symmetries of a
`~qsymm.model.Model`. Let us find out whether the 3D Rashba Hamiltonian defined earlier
has cubic group symmetry:

.. jupyter-execute::

    discrete_symm, continuous_symm = qsymm.symmetries(H, cubic_group)
    print(len(discrete_symm), len(continuous_symm))

It has 48 discrete symmetries (cubic group without inversion and time-reversal) and
no continuous symmetries (conserved quantities).

For more detailed examples see :ref:`tutorial_symmetry_finder` and :ref:`tutorial_kekule`.


Generating Hamiltonians from symmetry constraints
-------------------------------------------------
The `qsymm.hamiltonian_generator` module contains algorithms for generating Hamiltonians from
symmetry constraints.

For example let us generate all 2-band :math:`k \cdot p` Hamiltonians with the same discrete
symmetries as the Rashba Hamiltonian that we found in the previous section:

.. jupyter-execute::

    family = qsymm.continuum_hamiltonian(discrete_symm, dim=3, total_power=2, prettify=True)
    qsymm.display_family(family)

It is exactly the Hamiltonian family we started with.

For more detailed examples see :ref:`tutorial_kdotp_generator`, :ref:`tutorial_bloch_generator`
and :ref:`tutorial_kekule`.


Saving and loading Qsymm models
-------------------------------------------------
Qsymm models and identified symmetries don't guarantee consistent ordering and basis selection
across multiple runs. To avoid irrerproducible results you may use the ``Model.tosympy`` method
and serialize the resulting sympy expression as shown below.

To save we do:

.. jupyter-execute::

    H2D_sympy = H2D.tosympy()

    with open("H2D.txt", "w") as f:
        f.write(repr(H2D_sympy))

To load we do:

.. jupyter-execute::

    with open("H2D.txt") as f:
        data = f.read()

    loaded_H2D = qsymm.Model(
        sympy.parsing.sympy_parser.parse_expr(data),
        momenta=['k_x', 'k_z']
    )
