<jupyter_start><jupyter_text>Introduction to QosyWelcome to `qosy`!This notebook will give a brief overview of the main objects in this package. Other notebooks will show how to use these to construct operators with desired symmetries.  OperatorStringsThe main building block of `qosy` are `OperatorString` objects. They represent linearly independent Hermitian operators and are used as basis vectors of vector spaces of quantum operators.They come in three types: Pauli strings, Fermion strings, and Majorana strings.A **Pauli string** is a product of Pauli matrices. For example, $$\hat{\sigma}^x_1 \hat{\sigma}^x_2$$is a Pauli string acting on two orbitals.A **Fermion string** is a product of Fermionic creation and anhillation operators plus its Hermitian conjugate. For example, $$ \hat{c}_2^\dagger \hat{c}_1 + H.c. $$is a Fermion string acting on two orbitals.A **Majorana string** is a product of Majorana Fermion operators, $\hat{a}_j=\hat{c}^\dagger_j + \hat{c}_j$, $\hat{b}_j = i\hat{c}^\dagger_j -i \hat{c}_j$, and $\hat{d}_j=\hat{I}-2\hat{c}^\dagger_j \hat{c}_j$. For example,$$ i\hat{a}_1 \hat{b}_2 \hat{d}_3 $$is a Majorana string acting on three orbitals.Such `OperatorStrings` can be easily created and manipulated in `qosy`. For example with the following code:<jupyter_code>import qosy as qy

op_string_p = qy.opstring('X 1 X 2')
op_string_f = qy.opstring('CDag 2 C 1')
op_string_m = qy.opstring('1j A 1 B 2 D 3')<jupyter_output><empty_output><jupyter_text>To access the names of the operators attached to each orbital or the orbital labels, you can use the `orbital_operators` and `orbital_labels` parameters:<jupyter_code>print(op_string_f.orbital_operators)
print(op_string_m.orbital_labels)<jupyter_output>['CDag' 'C']
[1 2 3]<jupyter_text>To convert between different types of `OperatorStrings`, you can use the `convert` method. This will return an `Operator` object (discussed below).<jupyter_code># Convert a Majorana string into a linear combination
# of Fermion strings.
op = qy.convert(op_string_m, 'Fermion')

print('{} =\n{}'.format(op_string_m,op))<jupyter_output>1j A 1 B 2 D 3  =
(-2+0j) (1.0 CDag 2 CDag 3 C 3 C 1 )
(1+0j) (1.0 CDag 2 C 1 )
(2+0j) (1.0 CDag 1 CDag 2 CDag 3 C 3 )
(-1+0j) (1.0 CDag 1 CDag 2 )<jupyter_text>BasesTo construct quantum operators with desired symmetries, we want to search for operators in a vector space of operators. One way we do this is by defining a `Basis` of `OperatorStrings` that span that space.A convenient method for constructing bases is the `cluster_basis` function, which constructs a basis of operator strings with support on up to $k$ orbitals on a given cluster.<jupyter_code># Consider a "cluster" made of three
# orbitals labeled by integers.
cluster_orbitals = [1,2,3]

# Construct a basis made of all possible 
# k-local Pauli strings on the cluster.
k       = [1,3]
op_type = 'Pauli'
basis   = qy.cluster_basis(k, cluster_orbitals, op_type)

# Print a description of the OperatorStrings in the basis.
print(basis)<jupyter_output>1.0 X 1 
1.0 Y 1 
1.0 Z 1 
1.0 X 2 
1.0 Y 2 
1.0 Z 2 
1.0 X 3 
1.0 Y 3 
1.0 Z 3 
1.0 X 1 X 2 X 3 
1.0 X 1 X 2 Y 3 
1.0 X 1 X 2 Z 3 
1.0 X 1 Y 2 X 3 
1.0 X 1 Y 2 Y 3 
1.0 X 1 Y 2 Z 3 
1.0 X 1 Z 2 X 3 
1.0 X 1 Z 2 Y 3 
1.0 X 1 Z 2 Z 3 
1.0 Y 1 X 2 X 3 
1.0 Y 1 X 2 Y 3 
1.0 Y 1 X 2 Z 3 
1.0 Y 1 Y 2 X 3 
1.0 Y 1 Y 2 Y 3 
1.0 Y 1 Y 2 Z 3 
1.0 Y 1 Z 2 X 3 
1.0 Y 1 Z 2 Y 3 
1.0 Y 1 Z 2 Z 3 
1.0 Z 1 X 2 X 3 
1.0 Z 1 X 2 Y 3 
1.0 Z 1 X 2 Z 3 
1.0 Z 1 Y 2 X 3 
1.0 Z 1 Y 2 Y 3 
1.0 Z 1 Y 2 Z 3 
1.0 Z 1 Z 2 X 3 
1.0 Z 1 Z 2 Y 3 
1.0 Z 1 Z 2 Z 3<jupyter_text>An alternative way to define a vector space of Operators is by using a basis of `Operators` (see below) rather than `OperatorStrings`. Currently, a "basis" of `Operators` is simply a `list` of `Operators`.Note that Pauli strings and Majorana strings form complete orthonormal bases of the vector spaces of spin-$1/2$ and fermionic operators, where the inner product is the Hilbert-Schmidt inner product. This makes them particularly useful for calculations.  OperatorsA general quantum operator can be written as a linear combinations of operator strings. In particular, a Hermitian operator $\hat{\mathcal{O}}$ can be written as $\hat{\mathcal{O}} = \sum_a g_a \hat{h}_a$ where $g_a$ are real coefficients and $\hat{h}_a$ are operator strings.There are two ways to represent an operator in `qosy`: 1. As an `Operator` object. 2. As a vector associated with a `Basis` of `OperatorStrings`. When performing frequent or expensive calculations, approach 2 is necessary as matrix-vector manipulations are efficient. However, when performing infrequent or inexpensive calculations, approach 1 is preferred since `Operator` objects can be easier to manipulate.For example, the operator $\sum_j \hat{\sigma}^z_j$ can be represented in the following two ways:<jupyter_code>import numpy as np

# Consider a system with 10 orbitals.
num_orbitals = 10
orbitals     = np.arange(num_orbitals)

### Approach 1: use an Operator object ###

# The coefficients in front of the OperatorStrings.
coeffs = np.ones(num_orbitals)

# The OperatorStrings $\hat{\sigma}^z_j$.
op_strings = [qy.opstring('Z {}'.format(orbital)) for orbital in orbitals]

# Create the operator.
totalZ = qy.Operator(coeffs, op_strings)

# Print the operator as an Operator object.
print('Operator form of totalZ = ')
print(totalZ)

### Approach 2: use a vector in a Basis ###

# Construct a basis made of 1-local Pauli strings on all orbitals.
k       = 1
op_type = 'Pauli'
basis   = qy.cluster_basis(k, orbitals, op_type)

# Populate the correct entries of a vector.
inds_totalZ = [ind for ind in range(len(basis)) if basis[ind].orbital_operators[0] == 'Z']
vector_totalZ = np.zeros(len(basis))
vector_totalZ[inds_totalZ] = 1.0

# Print the operator as a vector in a basis.
print('Vector form of totalZ = ')
print(vector_totalZ)

# ...and the basis it is represented in.
print('Basis vectors = ')
print(basis)<jupyter_output>Operator form of totalZ = 
(1+0j) (1.0 Z 0 )
(1+0j) (1.0 Z 1 )
(1+0j) (1.0 Z 2 )
(1+0j) (1.0 Z 3 )
(1+0j) (1.0 Z 4 )
(1+0j) (1.0 Z 5 )
(1+0j) (1.0 Z 6 )
(1+0j) (1.0 Z 7 )
(1+0j) (1.0 Z 8 )
(1+0j) (1.0 Z 9 )

Vector form of totalZ = 
[ 0.  0.  1.  0.  0.  1.  0.  0.  1.  0.  0.  1.  0.  0.  1.  0.  0.  1.
  0.  0.  1.  0.  0.  1.  0.  0.  1.  0.  0.  1.]
Basis vectors = 
1.0 X 0 
1.0 Y 0 
1.0 Z 0 
1.0 X 1 
1.0 Y 1 
1.0 Z 1 
1.0 X 2 
1.0 Y 2 
1.0 Z 2 
1.0 X 3 
1.0 Y 3 
1.0 Z 3 
1.0 X 4 
1.0 Y 4 
1.0 Z 4 
1.0 X 5 
1.0 Y 5 
1.0 Z 5 
1.0 X 6 
1.0 Y 6 
1.0 Z 6 
1.0 X 7 
1.0 Y 7 
1.0 Z 7 
1.0 X 8 
1.0 Y 8 
1.0 Z 8 
1.0 X 9 
1.0 Y 9 
1.0 Z 9<jupyter_text>`Bases` and `Operators` come with convenient functionality.`Bases` can be expanded by adding `OperatorStrings` and other `Bases` to them directly:<jupyter_code># Construct a new empty Basis.
new_basis = qy.Basis()
# Add the above Basis's OperatorStrings to the new basis.
new_basis += basis
# Add an OperatorString directly to the basis.
new_basis += qy.opstring('X 1 X 2')

print(new_basis)<jupyter_output>1.0 X 0 
1.0 Y 0 
1.0 Z 0 
1.0 X 1 
1.0 Y 1 
1.0 Z 1 
1.0 X 2 
1.0 Y 2 
1.0 Z 2 
1.0 X 3 
1.0 Y 3 
1.0 Z 3 
1.0 X 4 
1.0 Y 4 
1.0 Z 4 
1.0 X 5 
1.0 Y 5 
1.0 Z 5 
1.0 X 6 
1.0 Y 6 
1.0 Z 6 
1.0 X 7 
1.0 Y 7 
1.0 Z 7 
1.0 X 8 
1.0 Y 8 
1.0 Z 8 
1.0 X 9 
1.0 Y 9 
1.0 Z 9 
1.0 X 1 X 2<jupyter_text>`Operators`, even if they are made up of different `OperatorStrings`, can be added together:<jupyter_code># Create another operator.
coeffs     = np.ones(num_orbitals)
op_strings = [qy.opstring('Y {}'.format(orbital)) for orbital in orbitals]
totalY     = qy.Operator(coeffs, op_strings)

# Add the two together.
sum_totalZ_totalY = totalZ + totalY

print(sum_totalZ_totalY)<jupyter_output>(1+0j) (1.0 Z 0 )
(1+0j) (1.0 Z 1 )
(1+0j) (1.0 Z 2 )
(1+0j) (1.0 Z 3 )
(1+0j) (1.0 Z 4 )
(1+0j) (1.0 Z 5 )
(1+0j) (1.0 Z 6 )
(1+0j) (1.0 Z 7 )
(1+0j) (1.0 Z 8 )
(1+0j) (1.0 Z 9 )
(1+0j) (1.0 Y 0 )
(1+0j) (1.0 Y 1 )
(1+0j) (1.0 Y 2 )
(1+0j) (1.0 Y 3 )
(1+0j) (1.0 Y 4 )
(1+0j) (1.0 Y 5 )
(1+0j) (1.0 Y 6 )
(1+0j) (1.0 Y 7 )
(1+0j) (1.0 Y 8 )
(1+0j) (1.0 Y 9 )
{'fraction_non_alphanumeric': 0.11443465309668611, 'fraction_numerical': 0.07459352116172273, 'mean_word_length': 3.2544878563885957, 'pattern_counts': {'":': 0, '<': 23, '<?xml version=': 0, '>': 23, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '130488', 'license': 'MIT', 'n_tokens_mistral': 3547, 'n_tokens_neox': 3236, 'n_words': 1335, 'path': 'tutorials/intro.ipynb', 'repo_name': 'ClarkResearchGroup/qosy', 'size': '8057'}