# 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/.
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.
"""
import numpy as np
from numpy import zeros, empty
from numpy import add, multiply
from numpy import exp
from sisl._internal import set_module
from sisl._help import dtype_real_to_complex
import sisl._array as _a
from sisl._array import aranged
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]}}}"
@property
def bloch(self):
""" Number of Bloch expansions along each lattice vector """
return self._bloch
[docs] def unfold_points(self, k):
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] = (aranged(B[0]).reshape(1, 1, -1) + k[0]) / B[0]
unfold[:, :, :, 1] = (aranged(B[1]).reshape(1, -1, 1) + k[1]) / B[1]
unfold[:, :, :, 2] = (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, *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):
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)