"""Operator factories and local gauge-invariant bases for the Z2 Fermi-Hubbard model."""
from itertools import product
import numpy as np
from scipy.sparse import csr_matrix
from scipy.sparse import identity as ID
from scipy.sparse import kron
from edlgt.modeling import LGT_border_configs, get_lattice_borders_labels
from edlgt.modeling import qmb_operator as qmb_op
from .bose_fermi_operators import fermi_operators
from .SU2_operators import SU2_generators
from .Zn_operators import Zn_rishon_operators
__all__ = [
"Z2_FermiHubbard_gauge_invariant_states",
"Z2_FermiHubbard_dressed_site_operators",
]
[docs]
def Z2_FermiHubbard_gauge_invariant_states(lattice_dim):
"""Construct local gauge-invariant basis states for the Z2 Fermi-Hubbard model.
Parameters
----------
lattice_dim : int
Number of spatial lattice dimensions.
Returns
-------
tuple
``(gauge_basis, gauge_states)`` dictionaries for bulk and border site
classes.
"""
if not np.isscalar(lattice_dim) or not isinstance(lattice_dim, int):
raise TypeError(
f"lattice_dim must be SCALAR & INTEGER, not {type(lattice_dim)}"
)
# List of borders/corners of the lattice
borders = get_lattice_borders_labels(lattice_dim)
# List of configurations for each element of the dressed site
single_rishon_configs = np.arange(2)
matter_config = np.array([0, 1, 1, 2])
dressed_site_config_list = [single_rishon_configs for _ in range(2 * lattice_dim)]
dressed_site_config_list.insert(0, matter_config)
core_labels = ["site"]
# Define useful quantities
gauge_states = {}
row = {}
col_counter = {}
for main_label in core_labels:
row_counter = -1
gauge_states[main_label] = []
row[main_label] = []
col_counter[main_label] = -1
for label in borders:
gauge_states[f"{main_label}_{label}"] = []
row[f"{main_label}_{label}"] = []
col_counter[f"{main_label}_{label}"] = -1
# Look at all the possible configurations of gauge links and matter fields
for config in product(*dressed_site_config_list):
# Update row counter
row_counter += 1
# Check Gauss Law
if sum(config) % 2 == 0:
# FIX row and col of the site basis
row[main_label].append(row_counter)
col_counter[main_label] += 1
# Save the gauge invariant state
gauge_states[main_label].append(config)
# Get the config labels
label = LGT_border_configs(config, 0, pure_theory=False)
if label:
# save the config state also in the specific subset for the specif border
for ll in label:
gauge_states[f"{main_label}_{ll}"].append(config)
row[f"{main_label}_{ll}"].append(row_counter)
col_counter[f"{main_label}_{ll}"] += 1
# Build the basis as a sparse matrix
gauge_basis = {}
for name in gauge_states:
data = np.ones(col_counter[name] + 1, dtype=float)
x = np.asarray(row[name])
y = np.arange(col_counter[name] + 1)
gauge_basis[name] = csr_matrix(
(data, (x, y)), shape=(row_counter + 1, col_counter[name] + 1)
)
# Save the gauge states as a np.array
gauge_states[name] = np.asarray(gauge_states[name])
return gauge_basis, gauge_states
[docs]
def Z2_FermiHubbard_dressed_site_operators(lattice_dim=2):
"""Build dressed-site operators for the Z2 Fermi-Hubbard model.
Parameters
----------
lattice_dim : int, optional
Number of spatial lattice dimensions.
Returns
-------
dict
Dictionary of dressed-site operators used by the model builder.
"""
if not np.isscalar(lattice_dim) and not isinstance(lattice_dim, int):
raise TypeError(
f"lattice_dim must be SCALAR & INTEGER, not {type(lattice_dim)}"
)
# Get the Rishon operators according to the chosen n representation s
in_ops = Zn_rishon_operators(2, False)
in_ops.update(fermi_operators(has_spin=True))
in_ops.update(SU2_generators(spin=1 / 2, matter=True))
in_ops["N_up_half"] = in_ops["N_up"] - 0.5 * ID(4)
in_ops["N_down_half"] = in_ops["N_down"] - 0.5 * ID(4)
in_ops["N_pair_half"] = in_ops["N_up_half"] * in_ops["N_down_half"]
# Dictionary for operators
ops = {}
# --------------------------------------------------------------------------------
# Rishon Number operators
for op in ["n", "P"]:
if lattice_dim == 1:
ops[f"{op}_mx"] = qmb_op(in_ops, [op, "IDz"])
ops[f"{op}_px"] = qmb_op(in_ops, ["IDz", op])
elif lattice_dim == 2:
ops[f"{op}_mx"] = qmb_op(in_ops, [op, "IDz", "IDz", "IDz"])
ops[f"{op}_my"] = qmb_op(in_ops, ["IDz", op, "IDz", "IDz"])
ops[f"{op}_px"] = qmb_op(in_ops, ["IDz", "IDz", op, "IDz"])
ops[f"{op}_py"] = qmb_op(in_ops, ["IDz", "IDz", "IDz", op])
elif lattice_dim == 3:
ops[f"{op}_mx"] = qmb_op(in_ops, [op, "IDz", "IDz", "IDz", "IDz", "IDz"])
ops[f"{op}_my"] = qmb_op(in_ops, ["IDz", op, "IDz", "IDz", "IDz", "IDz"])
ops[f"{op}_mz"] = qmb_op(in_ops, ["IDz", "IDz", op, "IDz", "IDz", "IDz"])
ops[f"{op}_py"] = qmb_op(in_ops, ["IDz", "IDz", "IDz", op, "IDz", "IDz"])
ops[f"{op}_px"] = qmb_op(in_ops, ["IDz", "IDz", "IDz", "IDz", op, "IDz"])
ops[f"{op}_pz"] = qmb_op(in_ops, ["IDz", "IDz", "IDz", "IDz", "IDz", op])
for op_name in ops:
ops[op_name] = kron(in_ops["ID_psi"], ops[op_name])
# --------------------------------------------------------------------------------
# Electric field operator
ops["E"] = 0
for s in "mp":
for d in "xyz"[:lattice_dim]:
ops["E"] += 0.5 * ops[f"P_{s}{d}"]
# --------------------------------------------------------------------------------
# Hopping operators
for s in ["up", "down"]:
if lattice_dim == 1:
ops[f"Q{s}_mx_dag"] = qmb_op(in_ops, [f"psi_{s}_dag_P", "Zm", "IDz"])
ops[f"Q{s}_px_dag"] = qmb_op(in_ops, [f"psi_{s}_dag_P", "P", "Zp"])
elif lattice_dim == 2:
ops[f"Q{s}_mx_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "Zm", "IDz", "IDz", "IDz"]
)
ops[f"Q{s}_my_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "Zm", "IDz", "IDz"]
)
ops[f"Q{s}_px_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "P", "Zp", "IDz"]
)
ops[f"Q{s}_py_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "P", "P", "Zp"]
)
elif lattice_dim == 3:
ops[f"Q{s}_mx_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "Zm", "IDz", "IDz", "IDz", "IDz", "IDz"]
)
ops[f"Q{s}_my_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "Zm", "IDz", "IDz", "IDz", "IDz"]
)
ops[f"Q{s}_mz_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "P", "Zm", "IDz", "IDz", "IDz", "IDz"]
)
ops[f"Q{s}_px_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "P", "P", "Zp", "IDz", "IDz"]
)
ops[f"Q{s}_py_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "P", "P", "P", "Zp", "IDz"]
)
ops[f"Q{s}_pz_dag"] = qmb_op(
in_ops, [f"psi_{s}_dag_P", "P", "P", "P", "P", "P", "Zp"]
)
# Add dagger operators
Qs = {}
for op_name, operator in ops.items():
dag_op = op_name.replace("_dag", "")
Qs[dag_op] = csr_matrix(operator.conj().transpose())
ops.update(Qs)
# --------------------------------------------------------------------------------
# Psi NUMBER OPERATORS
for label in [
"N_up",
"N_down",
"N_tot",
"N_single",
"N_pair",
"N_pair_half",
"Sz_psi",
"S2_psi",
]:
ops[label] = qmb_op(
in_ops, [label] + ["IDz" for _ in range(2 * lattice_dim)]
)
# --------------------------------------------------------------------------------
if lattice_dim == 2:
# LOCAL OPERATOR WITH THE SUM OF RISHON NUMBERS ALONG EACH LINK
ops["n_total"] = 0
for s in "mp":
for d in "xyz"[:lattice_dim]:
ops["n_total"] += ops[f"n_{s}{d}"]
# Sigma X Cross Operator
ops["X_Cross"] = qmb_op(in_ops, ["ID_psi", "P", "P", "P", "P"])
# ----------------------------------------------------------------------------
# Corner Operators
ops["C_pxpy"] = qmb_op(in_ops, ["ID_psi", "IDz", "IDz", "Zp_P", "Zp"])
ops["C_pymx"] = qmb_op(in_ops, ["ID_psi", "P_Zm_dag", "P", "P", "Zp"])
ops["C_mxmy"] = qmb_op(in_ops, ["ID_psi", "Zm_P", "Zm", "IDz", "IDz"])
ops["C_mypx"] = qmb_op(in_ops, ["ID_psi", "IDz", "Zm_P", "Zp_dag", "IDz"])
# ----------------------------------------------------------------------------
# Topological Operator along axis
ops["Sz_mypy"] = qmb_op(in_ops, ["ID_psi", "IDz", "Zm_dag_P", "P", "Zp"])
ops["Sz_mxpx"] = qmb_op(in_ops, ["ID_psi", "Zm_dag_P", "P", "Zp", "IDz"])
ops["Sx_pymy"] = qmb_op(in_ops, ["ID_psi", "IDz", "P", "IDz", "P"])
ops["Sx_pxmx"] = qmb_op(in_ops, ["ID_psi", "P", "IDz", "P", "IDz"])
return ops