Parameterized TBModel#
This notebook shows how to build parameterized tight-binding models in PythTB, how to evaluate them for different parameter values, and how to freeze those parameters into the model permanently.
Remember
Throughout, keep in mind that very parameter value is consumed as a scalar (or a 1‑D array of scalars for sweeps).
What you will learn
Define symbolic hopping/onsite terms.
Explore parameter sweeps in evaluation routines (Hamiltonian, eigenvalues, etc.).
Permanently “freeze” parameters with
TBModel.set_parametersand the copy-returningTBModel.with_parameters.Extend to spinful models, demonstrating how to supply \(2\times 2\) matrix blocks parametrically.
import numpy as np
from pythtb import TBModel, Lattice
Spinless model with a symbolic hopping#
We begin with a familiar two-orbital spinless model on a honeycomb-like lattice. The nearest-neighbor hopping is parameterised by a single symbol t.
lat_vecs = [[1.0, 0.0], [0.5, np.sqrt(3.0) / 2.0]]
orb_vecs = [[0.0, 0.0], [1.0 / 3.0, 1.0 / 3.0]]
lattice = Lattice(lat_vecs, orb_vecs, periodic_dirs=...)
tb = TBModel(lattice)
# Fixed on-site energies
tb.set_onsite([0.0, 0.0])
# Symbolic nearest-neighbour hopping: string 't'
tb.set_hop("t", 0, 1, [0, 0])
tb.set_hop(0.3, 1, 0, [1, 0])
tb.set_hop(0.1, 1, 0, [0, 1])
At this stage, the model knows it depends on a parameter named t. Notice in the printout below that the hopping from site 0 to site 1 with lattice vector [0, 0] is given as t, while the other hoppings are numeric values.
tb.parameters
[{'kind': 'hopping', 'orbitals': (0, 1), 'R': (0, 0), 'names': ('t',)}]
print(tb)
If we try to, for example, build the Hamiltonian without providing t, PythTB raises an error because the symbol has no value.
try:
tb.hamiltonian()
except ValueError as exc:
print(exc)
Missing parameter value(s): t
Providing parameter values during evaluation#
Every evaluation routine (hamiltonian, solve_ham, velocity, etc.) accepts keyword arguments for parameters. Values can be scalars or one-dimensional arrays of scalars.
k_mesh = tb.k_uniform_mesh([20, 20])
# Single value (scalar)
H_single = tb.hamiltonian(k_mesh, t=0.5)
# Sweep over five scalar values (1-D array of scalars)
t_values = np.linspace(-1.0, 1.0, 5)
H_sweep = tb.hamiltonian(k_mesh, t=t_values)
print("Hamiltonian shape with t=0.5:")
print(H_single.shape)
print("Hamiltonian shape sweeping over 5 t values:")
print(H_sweep.shape)
Hamiltonian shape with t=0.5:
(400, 2, 2)
Hamiltonian shape sweeping over 5 t values:
(400, 5, 2, 2)
The sweep result has the parameter axis tacked on after the k-point axis. Here, we have 5 values of t for each of the 400 k-points, resulting in a Hamiltonian array of shape (400, 5, 2, 2).
solve_ham works with the same pattern.
evals_single = tb.solve_ham(k_mesh, t=0.5) # passing a single scalar value
evals_sweep = tb.solve_ham(k_mesh, t=t_values) # passing a 1-D array of scalars
print("Energies shape with t=0.5:")
print(evals_single.shape)
print("Energies shape sweeping over 5 t values:")
print(evals_sweep.shape)
Energies shape with t=0.5:
(400, 2)
Energies shape sweeping over 5 t values:
(400, 5, 2)
The velocity method also accepts parameter values in the same way. An important caveat is when providing parameter sweeps: the derivative of the Hamiltonian with respect to the range of parameters is computed via finite differences and appended to the leading axis alongside the k-derivatives.
Notice below
The velocity array has shape (2, 400, 2, 2) when passing a scalar t, but shape (3, 400, 5, 2, 2) when passing a sweep of 5 t values. An extra axis for the parameter values has been added after the k-values, and the finite-difference derivative with respect to t has been tacked onto the front axis.
velocity_single = tb.velocity(k_mesh, t=0.5)
velocity_sweep = tb.velocity(k_mesh, t=t_values)
print("Velocity shape with t=0.5:")
print(velocity_single.shape)
print("Velocity shape sweeping over 5 t values:")
print(velocity_sweep.shape)
Velocity shape with t=0.5:
(2, 400, 2, 2)
Velocity shape sweeping over 5 t values:
(3, 400, 5, 2, 2)
Freezing parameters in place#
TBModel.set_parameters replaces symbolic providers (strings or callables) with the numeric value you supply. After calling it, those terms become permanent numbers.
Important
Every parameter passed to set_parameters must be a scalar. Arrays of scalars are only supported in the evaluation routines, not for freezing.
tb.set_parameters(t=0.8)
tb.parameters
[]
print(tb)
# Once frozen, the model no longer expects 't'
H_frozen = tb.hamiltonian(k_mesh)
print("Hamiltonian shape after freezing parameters:")
print(H_frozen.shape)
Hamiltonian shape after freezing parameters:
(400, 2, 2)
If you prefer not to modify the original model, use TBModel.with_parameters. It returns a copy with the specified parameters frozen.
tb_symbolic = TBModel(lattice)
tb_symbolic.set_onsite([0.0, 0.0])
tb_symbolic.set_hop("t", 0, 1, [0, 0])
tb_symbolic.set_hop(0.3, 1, 0, [1, 0])
tb_symbolic.set_hop(0.1, 1, 0, [0, 1])
tb_frozen = tb_symbolic.with_parameters(t=0.3)
# tb_symbolic still demands a parameter, tb_frozen does not
try:
tb_symbolic.hamiltonian(k_mesh)
except ValueError as exc:
print("symbolic:", exc)
H_frozen = tb_frozen.hamiltonian(k_mesh)
print("Frozen Hamiltonian shape:")
print(H_frozen.shape)
symbolic: Missing parameter value(s): t
Frozen Hamiltonian shape:
(400, 2, 2)
Spinless model with multiple symbolic terms#
This time, we create a model with multiple symbolic hopping terms. The parameters may take the same name or different names.
Those with the same name will share the same value when evaluated. Below, both orbitals share the onsite parameter m, and two hoppings have different parameter names t1 and t2.
lat_vecs = [[1, 0], [0, 1]]
orb_vecs = [[0, 0], [1 / 2, 1 / 2]]
lat = Lattice(lat_vecs=lat_vecs, orb_vecs=orb_vecs, periodic_dirs=...)
model = TBModel(lattice=lat, spinful=False)
model.set_onsite("m", ind_i=0)
model.set_onsite("m", ind_i=1)
model.set_hop("t1", 0, 1, [0, 0])
model.set_hop("t2", 1, 0, [0, 1])
print(model)
Below we will set two of the parameters to specific values, and leave t2 as a free parameter to be specified during evaluation.
model.set_parameters(m=1, t1=0.8)
print(model)
If we want to reinstate a parameter as symbolic after freezing it, we can use the same set_onsite and set_hop methods as when building the model initially. Here, we set the onsite energies back to depend on the parameter m.
model.set_onsite("m", ind_i=0)
model.set_onsite("m", ind_i=1)
Now we can pass m as a varying parameter during evaluation and set t2 to a fixed value.
H = model.hamiltonian(k_mesh, m=np.linspace(0, 1, 20), t2=0.4)
print("Hamiltonian shape with varying m:")
print(H.shape)
Hamiltonian shape with varying m:
(400, 20, 2, 2)
Or, we can pass both m and t2 as varying parameters.
H = model.hamiltonian(k_mesh, m=np.linspace(0, 1, 20), t2=np.linspace(0.1, 0.5, 10))
print("Hamiltonian shape with varying m and t2:")
print(H.shape)
Hamiltonian shape with varying m and t2:
(400, 20, 10, 2, 2)
The shape of the Hamiltonian (or any other observables that accept parameter sweeps) will reflect the order in which the varying parameters are provided. Here, m varies over 20 values and t2 over 10 values, resulting in a Hamiltonian array of shape (400, 20, 10, 2, 2). Instead, if we had provided t2 first, the shape would be (400, 10, 20, 2, 2).
H = model.hamiltonian(k_mesh, t2=np.linspace(0, 1, 10), m=np.linspace(0.1, 0.5, 20))
print("Hamiltonian shape with varying t2 and m:")
print(H.shape)
Hamiltonian shape with varying t2 and m:
(400, 10, 20, 2, 2)
Spinless model with callable parameters#
If we want more control over how parameters are evaluated, we can use callables instead of strings when building the model. Below, we create a model similar to the first example, but with a hopping term defined by a function of t.
Tip
A convenient way to define simple parameter functions is to use lambda functions, as shown below. lambda functions are anonymous functions defined in a single line. The syntax is lambda arguments: expression. The names of the arguments correspond to the parameter names used when evaluating the model.
lat_vecs = [[1.0, 0.0], [0.5, np.sqrt(3.0) / 2.0]]
orb_vecs = [[0.0, 0.0], [1.0 / 3.0, 1.0 / 3.0]]
lattice = Lattice(lat_vecs, orb_vecs, periodic_dirs=...)
tb = TBModel(lattice)
# Fixed on-site energies
tb.set_onsite([0.0, 0.0])
# Hopping with a callable parameter
tb.set_hop(lambda t: np.cos(t), 0, 1, [0, 0])
tb.set_hop(0.3, 1, 0, [1, 0])
tb.set_hop(0.1, 1, 0, [0, 1])
print(tb)
Everything else works the same way as with string parameters. We specify value(s) for the parameter t when evaluating the model.
tb.set_parameters(t=np.pi)
print(tb)
We can do more complicated things with callables, such as using mutliple parameters for a single callable, some of which may be shared with other symbolic terms. Here, we create a model with two symbolic hoppings defined by callables that share parameters r, theta and phi.
lat_vecs = [[1.0, 0.0], [0.5, np.sqrt(3.0) / 2.0]]
orb_vecs = [[0.0, 0.0], [1.0 / 3.0, 1.0 / 3.0]]
lattice = Lattice(lat_vecs, orb_vecs, periodic_dirs=...)
tb = TBModel(lattice)
# Fixed on-site energies
tb.set_onsite([0.0, 0.0])
# Hopping with a callable parameter
tb.set_hop(lambda r, theta, phi: r * np.sin(theta) * np.sin(phi), 0, 1, [0, 0])
tb.set_hop(lambda r, theta, phi: r * np.sin(theta) * np.cos(phi), 1, 0, [1, 0])
tb.set_hop(lambda r, theta: r * np.cos(theta), 1, 0, [0, 1])
print(tb)
tb.set_parameters(r=0.5, theta=np.pi / 4, phi=np.pi / 3)
print(tb)
Spinful model with parameterized \(2\times 2\) blocks#
Spinful on-site and hopping blocks are \(2\times 2\) Hermitian matrices. You can still parameterize them, but each matrix needs to be built from scalar coefficients.
Trying to pass a matrix directly into set_parameters fails.
lat_spin = Lattice([[1.0]], [[0.0]], periodic_dirs=[0])
spin_tb = TBModel(lat_spin, spinful=True)
spin_tb.set_hop("t_spin", 0, 0, [1])
try:
spin_tb.set_parameters(t_spin=np.array([[0.1, 0.2j], [-0.2j, -0.1]]))
except TypeError as exc:
print(exc)
Parameter 't_spin' must be a scalar.
Instead, we would need to use a callable that sets the desired matrix element symbolically. For example, we can define a spinful onsite block with parameters a and b as follows:
lat_spin = Lattice([[1.0]], [[0.0]], periodic_dirs=[0])
spin_tb = TBModel(lat_spin, spinful=True)
spin_tb.set_hop(lambda a, b: np.array([[a, b], [np.conjugate(b), a]]), 0, 0, [1])
We could also only parameterize some of the matrix elements, while keeping others fixed. Here, we define a hopping block with fixed diagonal elements and parameterized off-diagonal elements.
lat_spin = Lattice([[1.0]], [[0.0]], periodic_dirs=[0])
spin_tb = TBModel(lat_spin, spinful=True)
spin_tb.set_hop(lambda b: np.array([[0.1, b], [np.conjugate(b), -0.1]]), 0, 0, [1])
The key point is that parameters represent scalar values, so the callable must return a something consistent with what set_hop and set_onsite expect: either a scalar (for spinless) or a \(2\times 2\) Hermitian matrix or Pauli vector (for spinful).
Pauli decomposition pattern#
Expose scalar coefficients for the Pauli basis (identity plus \(\sigma_x\), \(\sigma_y\), \(\sigma_z\)).
spin_tb = TBModel(lat_spin, spinful=True)
spin_tb.set_hop(
lambda t0, t1, t2, t3: [
t0,
t1,
t2,
t3,
], # internally converts Pauli vector to 2x2 matrix
0,
0,
[1],
)
spin_tb.set_parameters(t0=0, t1=1, t2=0, t3=1)
print(spin_tb)
----------------------------------------
Tight-binding model report
----------------------------------------
r-space dimension = 1
k-space dimension = 1
periodic directions = [0]
spinful = True
number of spin components = 2
number of electronic states = 2
number of orbitals = 1
Lattice vectors (Cartesian):
# 0 ===> [ 1.000]
Volume of unit cell (Cartesian) = 1.000 [A^d]
Reciprocal lattice vectors (Cartesian):
# 0 ===> [ 6.283]
Volume of reciprocal unit cell = 6.283 [A^-d]
Orbital vectors (Cartesian):
# 0 ===> [ 0.000]
Orbital vectors (fractional):
# 0 ===> [ 0.000]
----------------------------------------
Site energies:
< 0 | H | 0 > = [[0.+0.j 0.+0.j] [0.+0.j 0.+0.j]]
Hoppings:
< 0 | H | 0 + [ 1.0 ] > = [[ 1.+0.j 1.+0.j] [ 1.+0.j -1.+0.j]]
Hopping distances:
| pos( 0 ) - pos( 0 ) + [ 1.0 ] | = 1.000
Solver calls and sweeps#
Evaluation routines also insist on scalar inputs (arrays for sweeps). Below we sweep t0 while keeping the other coefficients fixed; the callable assembles the matrix internally for each scalar.
spin_tb = TBModel(lat_spin, spinful=True)
spin_tb.set_hop(
lambda t0, t1, t2, t3: [t0, t1, t2, t3],
0,
0,
[1],
)
t0_vals = np.linspace(-0.2, 0.2, 5)
H_spin = spin_tb.hamiltonian(
k_pts=[[0], [0.5], [1]], t0=t0_vals, t1=0.05, t2=0.0, t3=-0.10
)
H_spin.shape # (Nk, len(t0_vals), norb, nspin, norb, nspin)
(3, 5, 1, 2, 1, 2)
Summary#
Every parameter value—whether supplied to
hamiltonianor toset_parameters— must be a scalar (or a 1D array of scalars for sweeps).Spinful blocks are produced by callables that assemble \(2\times 2\) matrices or Pauli vectors from those scalars.
set_parameters/with_parameterspermanently freeze symbolic terms; solver kwargs apply only for that evaluation.
With these patterns you can combine strings, callables, and shared parameter names confidently across both spinless and spinful tight-binding models.