# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
from __future__ import annotations
r"""Bloch's theorem
===================
Bloch's theorem is a very powerful proceduce that enables one to utilize
the periodicity of a given direction to describe the complete system.
"""
from typing import Sequence
import numpy as np
from numpy import add, empty, exp, multiply, zeros
import sisl._array as _a
from sisl._help import dtype_real_to_complex
from sisl._internal import set_module
from sisl.typing import KPoint
from ._bloch import bloch_unfold
__all__ = ["Bloch"]
@set_module("sisl.physics")
class Bloch:
r""" Bloch's theorem object containing unfolding factors and unfolding algorithms
This class is a wrapper for expanding *any* matrix from a smaller matrix cell into
a larger, using Bloch's theorem.
The general idea may be summarized in the following equation:
.. math::
\mathbf M_{K}^N =\frac1N
\;
\sum_{
\substack{j=0\\
k_j=\frac{K+j}N
}
}^{N-1}
\quad
\begin{bmatrix}
1 & \cdots & e^{i (1-N)k_j}
\\
e^{i k_j} & \cdots & e^{i (2-N)k_j}
\\
\vdots & \ddots & \vdots
\\
e^{i (N-1)k_j} & \cdots & 1
\end{bmatrix}
\otimes
\mathbf M_{k_j}^1.
Parameters
----------
bloch : (3,) int
Bloch repetitions along each direction
Examples
--------
>>> bloch = Bloch([2, 1, 2])
>>> k_unfold = bloch.unfold_points([0] * 3)
>>> M = [func(*args, k=k) for k in k_unfold]
>>> bloch.unfold(M, k_unfold)
"""
[docs]
def __init__(self, *bloch):
"""Create `Bloch` object"""
self._bloch = _a.arrayi(bloch).ravel()
self._bloch = np.where(self._bloch < 1, 1, self._bloch).astype(
np.int32, copy=False
)
if len(self._bloch) != 3:
raise ValueError(self.__class__.__name__ + " requires 3 input values")
if np.any(self._bloch < 1):
raise ValueError(
self.__class__.__name__ + " requires all unfoldings to be larger than 0"
)
def __len__(self):
"""Return unfolded size"""
return np.prod(self.bloch)
def __str__(self):
"""Representation of the Bloch model"""
B = self._bloch
return f"{self.__class__.__name__}{{{B[0]}, {B[1]}, {B[2]}}}"
def __repr__(self):
"""Representation of the Bloch model"""
B = self._bloch
cls = self.__class__
return f"<{cls.__module__}.{cls.__name__}{{{B[0]}, {B[1]}, {B[2]}}}>"
@property
def bloch(self):
"""Number of Bloch expansions along each lattice vector"""
return self._bloch
[docs]
def unfold_points(self, k: KPoint):
r"""Return a list of k-points to be evaluated for this objects unfolding
The k-point `k` is with respect to the unfolded geometry.
The return list of `k` points are the k-points required to be sampled in the
folded geometry.
Parameters
----------
k : (3,) of float
k-point evaluation corresponding to the unfolded unit-cell
Returns
-------
numpy.ndarray
a list of ``np.prod(self.bloch)`` k-points used for the unfolding
"""
k = _a.arrayd(k)
# Create expansion points
B = self._bloch
unfold = _a.emptyd([B[2], B[1], B[0], 3])
# Use B-casting rules (much simpler)
unfold[:, :, :, 0] = (_a.aranged(B[0]).reshape(1, 1, -1) + k[0]) / B[0]
unfold[:, :, :, 1] = (_a.aranged(B[1]).reshape(1, -1, 1) + k[1]) / B[1]
unfold[:, :, :, 2] = (_a.aranged(B[2]).reshape(-1, 1, 1) + k[2]) / B[2]
# Back-transform shape
return unfold.reshape(-1, 3)
[docs]
def __call__(self, func, k: KPoint, *args, **kwargs):
"""Return a functions return values as the Bloch unfolded equivalent according to this object
Calling the `Bloch` object is a shorthand for the manual use of the `Bloch.unfold_points` and `Bloch.unfold`
methods.
This call structure is a shorthand for:
>>> bloch = Bloch([2, 1, 2])
>>> k_unfold = bloch.unfold_points([0] * 3)
>>> M = [func(*args, k=k) for k in k_unfold]
>>> bloch.unfold(M, k_unfold)
Notes
-----
The function passed *must* have a keyword argument ``k``.
Parameters
----------
func : callable
method called which returns a matrix.
k : (3, ) of float
k-point to be unfolded
*args : list
arguments passed directly to `func`
**kwargs : dict
keyword arguments passed directly to `func`
Returns
-------
numpy.ndarray
unfolded Bloch matrix
"""
K_unfold = self.unfold_points(k)
M0 = func(*args, k=K_unfold[0, :], **kwargs)
shape = (K_unfold.shape[0], M0.shape[0], M0.shape[1])
M = empty(shape, dtype=dtype_real_to_complex(M0.dtype))
M[0] = M0
del M0
for i in range(1, K_unfold.shape[0]):
M[i] = func(*args, k=K_unfold[i, :], **kwargs)
return bloch_unfold(self._bloch, K_unfold, M)
[docs]
def unfold(self, M, k_unfold: Sequence[KPoint]):
r"""Unfold the matrix list of matrices `M` into a corresponding k-point (unfolding k-points are `k_unfold`)
Parameters
----------
M : (:, :, :)
an ``*``-N-M matrix used for unfolding
k_unfold : (:, 3) of float
unfolding k-points as returned by `Bloch.unfold_points`
Returns
-------
numpy.ndarray
unfolded matrix of size ``M[0].shape * k_unfold.shape[0] ** 2``
"""
if isinstance(M, (list, tuple)):
M = np.stack(M)
return bloch_unfold(self._bloch, k_unfold, M)