# PythTB 2.0.0 Release Notes

PythTB 2.0.0 is a major redesign of the package, focused on long-term maintainability, performance, and a modern API. It introduces:

- A modular package architecture
- API cleanup and unification
- Performance improvements via vectorization with significant speedups for large calculations
- New `Lattice`, `Mesh`, and `Wannier` classes
- Expanded functionality: maximally localized and disentangled Wannier functions, Quantum geometric tensor, 
  quantum metric, local Chern marker, axion angle, and more
- Wannier90 + Quantum ESPRESSO I/O utilities
- Type hints and improved error handling
- Improved testing and documentation
- New readthedocs-based [documentation site](https://pythtb.readthedocs.io/en/dev/)

This document highlights the design philosophy, API changes, and migration guidance. For a full list of changes, see the [Changelog](https://pythtb.readthedocs.io/en/dev/CHANGELOG.html). For detailed usage, see the updated [documentation](https://pythtb.readthedocs.io/en/dev/) and [tutorials](https://pythtb.readthedocs.io/en/dev/tutorials/index.html).

## Why PythTB 2.0.0?

The original package had a single-file (`pythtb.py`) design that served early use cases well. However, as the codebase expanded, this structure became unwieldy. Version 2.0.0 reorganizes the project into a modular structure with a more intuitive API and consistent naming, following PEP-8 and scientific Python community best practices.

The goals:

- Improve readability and maintainability by breaking the code into smaller, focused modules.
- Enhance performance through vectorized operations, enabling efficient computations on larger systems.
- Provide a cleaner, more consistent API that is easier to learn and use.
- Enable community contributions by establishing clear project structure and guidelines.
- Establish a stable surface for future development

## Major Changes

### Version Support

**What Changed:**  
- Dropped support for Python <3.12 and NumPy <2.0

**Rationale:**
- Allows use of latest syntax and features (match-case, improved typing, etc.).
- Many scientific libraries have dropped support for older versions.
- Follows [SPEC-0](https://scientific-python.org/specs/spec-0000/): Scientific Python minimum standards.

### Modular Package Structure

**Before:** everything in a single `pythtb.py` file.

**Now:** split into multiple modules under the `pythtb/` package: 

Module | Purpose
---    |  ---
`tbmodel.py` | Tight-binding model class - `TBModel`
`wfarray.py` | Wavefunction array - `WFArray`
`mesh.py` | k-point and adiabatic parameter mesh construction and related routines
`w90.py` | `W90` class for Wannier90 interfacing
`lattice.py` | Lattice geometry and related routines
`hoptable.py` | Hopping terms storage and manipulation
`utils.py` | General-purpose helpers
`visualization/` | Plotting utilities, not part of public API
`models/` | Canonical tight-binding model library (graphene, Haldane, SSH, etc.)
`io/wannier90.py` | Wannier90 file readers
`io/qe.py` | Quantum ESPRESSO file readers

**Rationale:**
- Separation of concerns -> easier maintenance, testing, and extension
- Aligns with [Python Packaging Authority best practices](https://packaging.python.org/en/latest/tutorials/packaging-projects/).
- See also: [Real Python: Python Application Layouts](https://realpython.com/python-application-layouts/)

### New Core Classes

#### `Lattice`

Encapsulates lattice geometry, orbital positions, and nearest-neighbour shells. Every model and wavefunction array references the same lattice instance to ensure consistent coordinates. A `Lattice` can be shared across multiple models and wavefunction arrays. The `Lattice` class also provides methods for visualizing the lattice in real and reciprocal space.

To create a `Lattice`:

```python
from pythtb import Lattice
a1 = [1.0, 0.0]
a2 = [0.0, 1.0]
tau_1 = [0.0, 0.0]
tau_2 = [0.5, 0.5]

lat = Lattice([a1, a2], [tau_1, tau_2], periodic_dirs=...)  # '...' sets all directions as periodic
lat = Lattice([a1, a2], [tau_1, tau_2])  # not specifying periodic_dirs defaults to a finite system 
lat = Lattice([a1, a2], [tau_1, tau_2], periodic_dirs=[0])  # We may set a subset of directions as periodic (as before)
```

#### `Mesh`
Describes k-space grids, paths, and parameter sweeps. Encodes topology (loops, endpoints, adiabatic cycles) so `WFArray` can apply appropriate gauge conditions downstream.

To create a `Mesh`:

```python
from pythtb import Mesh

mesh = Mesh(dim_k=2, axis_types=['k', 'k'])
mesh.build_grid([10, 10], gamma_centered=True)  # builds a 10x10 k-point grid centered at Gamma

mesh = Mesh(dim_k=1, axis_types=['k'])
mesh.build_path([[0, 0], [0.5, 0], [0.5, 0.5]], [50, 50])  # builds a k-point path

mesh = Mesh(dim_k=1, dim_param=1, axis_types=['k', 'param'])  # k-point + parameter sweep
mesh.build_grid([100, 20])  # builds a grid with 100 k-points and 20 parameter values

# makes the parameter axis a loop (adiabatic cycle) looping the second (index 1) 
# component of the mesh vector back to itself along the second axis (index 1)
mesh.loop(1, 1) 

# Manual point specification
mesh = Mesh(dim_k=1, axis_types=['k'])
k_pts = np.linspace(0, 1, 100, endpoint=True)  # endpoint=True includes the last point
mesh.build_custom(k_pts)
# winds kx around the BZ and makes the path closed
mesh.loop(0, 0, winds_bz=True, closed=True)  
```

#### `Wannier`
Build Wannier gauges directly inside PythTB from a `WFArray`, perform projections, disentanglement and maximal localization, and analyze spreads and centers.

```python
from pythtb import Wannier

wannier = Wannier(wf_array)

# Define trial functions as list of (orbital index, weight) tuples
tf_list = [[(orb, weight)] for orb, weight in zip(orbital_indices, weights)]

# project onto occupied bands
wannier.project(tf_list, band_idxs=list(range(n_occ)))  

# disentanglement procedure
frozen_window = [E_min, E_max]  # define frozen window energies
outer_window = [E_min_outer, E_max_outer]  # define outer window energies
wannier.disentangle(
  n_wfs=2, 
  frozen_window=frozen_window,
  outer_window=outer_window,
  max_iter=1000,
  tol=1e-6
  )

wannier.project(use_tilde=True)  # re-project using disentangled states

# maximal localization
wannier.maxloc(
  alpha=1/2, 
  max_iter=1000, 
  tol=1e-10, 
  grad_min=1e-10, 
  verbose=True)  

```

### API Cleanup & Naming Consistency

#### Renaming Classes

Renamed classes using PascalCase per [PEP 8](https://peps.python.org/pep-0008/#class-names) conventions

< v2.0 | v2.0
---    |  ---
`tb_model` | `TBModel`
`wf_array` | `WFArray`
`w90` | `W90`

Dropped redundant `object` base class (i.e., `class TBModel(object):` is now `class TBModel:`). All classes are new-style in Python 3.

**Rationale:**
- Follows community conventions.
- Improves distinguishability of classes and functions.

#### Initialization Patterns

The `Lattice` and `Mesh` classes were introduced to encapsulate lattice geometry and k-point/parameter mesh construction, respectively. This separates concerns and improves code organization. These classes can also be used as standalone utilities and shared across multiple models.

< v2.0 | 2.0 | Purpose
---    |  --- |  ---
`tb_model(dim_r, dim_k, lat, orb, per, nspin)` | `TBModel(Lattice, spinful)` | Lattice geometry and orbital positions are encapsulated in a `Lattice` instance.
`wf_array(model, mesh_arr, nsta_arr)` | `WFArray(Lattice, Mesh, nstates)` |  Decouples wavefunction storage from model definition. `Mesh` encodes k-point and parameter grids.

**Rationale:**
- Separation of concerns -> easier maintenance and testing.
- Reusable components for advanced workflows.
- Aligns with object-oriented design principles.

#### Method Renaming & Unification

Many methods were renamed for clarity and consistency. Some methods were merged to reduce redundancy.

**Examples:**

< v2.0 | v2.0 | Notes
---    |  ---   | ---
`tb_model.solve_one` & `tb_model.solve_all` | `TBModel.solve_ham` | Unified method for solving Hamiltonian at single/multiple k-points
`wf_array.berry_phase` parameter `dir` | Renamed to `axis_idx` to avoid conflict with Python keyword `dir` | Prevents syntax issues and improves clarity
`w90.w90_bands_consistency` | Renamed to `W90.bands_w90` | Brevity and clarity

**Rationale:**
- Less functions to learn and remember.
- Predictable, explicit naming make the library easier to use and less error-prone ([Zen of Python, PEP 20](https://peps.python.org/pep-0020/)).
- [API Design Guidelines](https://docs.python-guide.org/writing/style/#api-design)

### Vectorization and Performance

**What Changed:**  
- Performance bottlenecks were addressed via NumPy vectorization.
  - For example, the `tb_model._gen_ham` and `tb_model._sol_ham` functions for generating and diagonalizing the Hamiltonian were a major bottleneck. It has been restructured to utilize NumPy vectorization, eliminating explicit `for` loops over the k-points.
- TensorFlow optional acceleration added for large-scale problems. TensorFlow accelerates linear algebra operations on GPUs. This is an optional dependency that can be enabled for users with compatible hardware. 

**Rationale:**
- Loops in Python are slow; NumPy operations are orders of magnitude faster ([Why NumPy is Fast](https://numpy.org/doc/stable/user/whatisnumpy.html#why-is-numpy-fast), [NumPy Best Practices](https://numpy.org/doc/stable/user/absolute_beginners.html), [SciPy Lecture Notes: Performance](https://scipy-lectures.org/advanced/optimizing/)).
- Enables calculations over large k-point grids and many bands.
- Output shapes now follow the most common convention (k-points as the leading axis).

### Parameterized Models

`TBModel` methods now support symbolic strings and callables for parameter-dependent onsite energies and hoppings. This enables defining models with tunable parameters (e.g., strain, electric field) that can be varied without reconstructing the entire model. 

**Example:**

```python
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 hoppings 't1' and 't2'
tb.set_hop("t1", 0, 1, [0, 0])
tb.set_hop("t2", 1, 0, [1, 0])
tb.set_hop(0.1, 1, 0, [0, 1])
```

Every evaluation routine (`hamiltonian`, `solve_ham`, `velocity`, etc.) accepts keyword arguments for parameters. Values can be **scalars** or **one-dimensional arrays of scalars**.

**Example:**

```python
k_mesh = tb.k_uniform_mesh([20, 20])

# Single value (scalar)
H_single = tb.hamiltonian(k_mesh, t1=0.5, t2=0.3)

# 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, t1=t_values, t2=t_values)

# passing a single scalar value
evals_single = tb.solve_ham(k_mesh, t1=0.5, t2=0.3) 
# passing a 1-D array of scalars
evals_sweep = tb.solve_ham(k_mesh, t1=t_values, t2=t_values) 
```

These evaluations leave the parameterized model unchanged, allowing repeated evaluations with different parameter values. These parameters can be set in place with numerical values using e.g.  `TBModel.set_parameters(t=0.8)`.

Parameters may also be defined as callables (functions) that take parameter values as input and return numerical values, or arrays/lists that are expected by `set_onsite` and `set_hop`. This allows for complex dependencies.

**Example:**

```python
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])
```

The parameter names are inferred from the argument names of the callable functions. So downstream evaluation methods accept keyword arguments matching those names e.g. `tb.hamiltonian(k_mesh, t=np.pi)`.

### Quantum Geometry Enhancements

`TBModel` and `WFArray` now include methods for computing quantum geometric properties:

- `TBModel.velocity()`: Velocity operator as derivative of Hamiltonian with respect to $k$ and parameters
- `TBModel.quantum_geometric_tensor()`: Quantum geometric tensor from Kubo formula
- `TBModel.berry_curvature()`: Berry curvature from quantum geometric tensor
- `TBModel.quantum_metric()`: Quantum metric from quantum geometric tensor
- `TBModel.chern_number()`: Chern number from integrated Berry curvature over 2D planes
- `TBModel.local_chern_marker()`: Bianco-Resta local Chern marker in finite systems
- `TBModel.axion_angle()`: Chern-Simons axion angle for 3D insulators with an adiabatic parameter using 4-curvature
- `WFArray.links()`: Computes the unitary link matrices between neighboring mesh points
- `WFArray.berry_connection()`: Berry connection from link matrices
- `WFArray.wilson_loop()`: Wilson loop unitary matrix along specified mesh axes, optionally returns unit-norm wilson loop eigenvalues 
- `WFArray.berry_curvature()`: Berry curvature from plaquette method, is dimensionful in contrast to `TBModel.berry_flux()`. Divides the flux by the area of the plaquette.
- `WFArray.chern_number()`: Chern number from integrated Berry curvature over 2D planes.

### Type Hints and Error Handling

**What Changed:** 
- Added type annotations throughout codebase ([PEP 484 – Type Hints](https://peps.python.org/pep-0484/))
- Replaced generic `raise Error` with specific exceptions, e.g., `TypeError` or `ValueError` 

**Rationale:**
- Type hints enable better linting and IDE support. ([Type Checking – Python Docs](https://docs.python.org/3/library/typing.html))
- Clear errors help users debug issues without digging into internals ([Effective Error Messages](https://nedbatchelder.com/text/errors.html))

### Logging vs. Print Statements

**What Changed:**  
- Replaced print-based messaging with Python’s `logging` module.

**Rationale:**
- Log messages can be filtered by severity (DEBUG, INFO, WARNING, ERROR).
- Users can control where log messages go (stdout, file, etc.).
- [Logging HOWTO – Python Docs](https://docs.python.org/3/howto/logging.html)
- [Real Python: Logging in Python](https://realpython.com/python-logging/)


## Packaging and Distribution

**What Changed:**  
Transitioned from `setup.py` to `pyproject.toml`, added standard files (`LICENSE`, `CHANGELOG`, `CONTRIBUTING`).
- `LICENSE`: GPL-3 license (copied over from the .txt file).
- `CHANGELOG`: This file for tracking changes between versions. 
- `pyproject.toml`: Now the recommended way to package Python projects.
- `CONTRIBUTING`: Outlines expectations and guidelines for contributors.

**Rationale:**
- `pyproject.toml` is the new standard for Python packaging.
- Tools like Poetry, Flit, and pip rely on standardized metadata.
-  `CONTRIBUTING` and `LICENSE` are now expected in open source.
- [Packaging Python Projects – Official Tutorial](https://packaging.python.org/en/latest/tutorials/packaging-projects/)
- [PEP 517 – pyproject.toml build-system](https://peps.python.org/pep-0517/)
- [GitHub Community Standards](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/)


## Testing and Continuous Integration

**What Changed:**
- Added a `tests/test_examples` directory with pytest-based regression tests for all examples
- Each test runs the example scripts and compares outputs to reference data from v1.8.
- Added a `tests/test_examples/make_test_example.py` to automatically generate a skeleton for new tests to be made in the future.
- Added a `tests/report_test_status.py` to print a summary of which tests are passing and the date/time of last pass/fail.

**Rationale:**
- Ensures backward compatibility and prevents regressions during refactors or feature additions
- Encourages contributors to include tests alongside new code

**If you have questions or want to propose a change, please open an [issue](https://github.com/pythtb/pythtb/issues) or start a [discussion](https://github.com/orgs/pythtb/discussions) .**  