tbkit 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {tbkit-0.2.0/tbkit.egg-info → tbkit-0.3.0}/PKG-INFO +19 -4
  2. {tbkit-0.2.0 → tbkit-0.3.0}/README.md +16 -2
  3. {tbkit-0.2.0 → tbkit-0.3.0}/pyproject.toml +10 -2
  4. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/__init__.py +1 -1
  5. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/error_handling.py +24 -34
  6. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/graphene.py +72 -34
  7. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/kspace.py +35 -12
  8. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/lattice.py +10 -12
  9. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/plot.py +41 -28
  10. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/propagation.py +14 -11
  11. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/save.py +14 -6
  12. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/system.py +48 -36
  13. {tbkit-0.2.0 → tbkit-0.3.0/tbkit.egg-info}/PKG-INFO +19 -4
  14. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit.egg-info/SOURCES.txt +1 -0
  15. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_error_handling.py +0 -7
  16. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_kspace.py +4 -1
  17. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_lattice.py +4 -1
  18. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_plot.py +6 -3
  19. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_propagation.py +26 -1
  20. tbkit-0.3.0/tests/test_regressions.py +355 -0
  21. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_save.py +9 -1
  22. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_system.py +20 -0
  23. {tbkit-0.2.0 → tbkit-0.3.0}/LICENSE +0 -0
  24. {tbkit-0.2.0 → tbkit-0.3.0}/setup.cfg +0 -0
  25. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/dos.py +0 -0
  26. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit/lattices.py +0 -0
  27. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit.egg-info/dependency_links.txt +0 -0
  28. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit.egg-info/requires.txt +0 -0
  29. {tbkit-0.2.0 → tbkit-0.3.0}/tbkit.egg-info/top_level.txt +0 -0
  30. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_dos.py +0 -0
  31. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_graphene.py +0 -0
  32. {tbkit-0.2.0 → tbkit-0.3.0}/tests/test_lattices.py +0 -0
@@ -1,8 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tbkit
3
- Version: 0.2.0
4
- Summary: A Python package to build and solve Tight-Binding models, for research and education.
3
+ Version: 0.3.0
4
+ Summary: A Python package to build and solve Tight-Binding models.
5
5
  Author-email: Charles Poli <cpoli374@gmail.com>
6
+ Maintainer-email: cpoli <cpoli374@gmail.com>
6
7
  License-Expression: BSD-3-Clause
7
8
  Project-URL: Homepage, https://github.com/cpoli/tbkit
8
9
  Classifier: Intended Audience :: Education
@@ -24,14 +25,15 @@ Requires-Dist: pydata-sphinx-theme>=0.15; extra == "docs"
24
25
  Requires-Dist: sphinx-gallery>=0.15; extra == "docs"
25
26
  Dynamic: license-file
26
27
 
27
- # tbkit — a Tight-Binding package for research and education
28
+ # tbkit — a Tight-Binding package
28
29
 
29
30
  [![tests](https://github.com/cpoli/tbkit/actions/workflows/tests.yml/badge.svg)](https://github.com/cpoli/tbkit/actions/workflows/tests.yml)
31
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://cpoli.github.io/tbkit/coverage/)
30
32
  [![docs](https://img.shields.io/badge/docs-cpoli.github.io%2Ftbkit-blue.svg)](https://cpoli.github.io/tbkit/)
31
33
  [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
32
34
  [![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)
33
35
 
34
- ![tbkit logo](docs/source/_static/image/tbkit_logo.png)
36
+ ![tbkit logo](https://raw.githubusercontent.com/cpoli/tbkit/master/docs/source/_static/image/tbkit_logo.png)
35
37
 
36
38
  **tbkit** is a Python package to build and solve Tight-Binding models, written
37
39
  in vectorized NumPy/SciPy. It aims to make the mechanics of Tight-Binding
@@ -85,6 +87,13 @@ research prototyping.
85
87
 
86
88
  Requires Python >= 3.10.
87
89
 
90
+ ```bash
91
+ pip install tbkit
92
+ ```
93
+
94
+ or, for an editable install from a clone (e.g. to run the test suite or
95
+ work on tbkit itself):
96
+
88
97
  ```bash
89
98
  git clone https://github.com/cpoli/tbkit
90
99
  cd tbkit
@@ -98,6 +107,9 @@ pip install -e ".[test]"
98
107
  pytest tests/
99
108
  ```
100
109
 
110
+ The test suite has 100% line coverage of the `tbkit` package; see the
111
+ [HTML coverage report](https://cpoli.github.io/tbkit/coverage/).
112
+
101
113
  ## Quick start
102
114
 
103
115
  Real-space flake, nearest-neighbor square lattice:
@@ -204,6 +216,7 @@ thumbnailed example gallery under `docs/source/api/gallery/`.
204
216
 
205
217
  | Script | What it shows |
206
218
  |---------------------------------------------------------------------------|----------------|
219
+ | [`tight_binding/plot_square_lattice_bands.py`](examples/tight_binding/plot_square_lattice_bands.py) | The square lattice: Bloch's theorem at its simplest, bands along Gamma-X-M-Gamma, the nested Fermi surface, and the van Hove singularity. |
207
220
  | [`tight_binding/plot_graphene_bands.py`](examples/tight_binding/plot_graphene_bands.py) | Real-space flake + reciprocal-space band structure; graphene's Dirac point and Wallace's 1947 linear dispersion. |
208
221
  | [`tight_binding/plot_visualizing_a_model.py`](examples/tight_binding/plot_visualizing_a_model.py) | `tbkit.plot.Plot`: lattice, spectrum with sublattice polarization, density of states, eigenstate intensity. |
209
222
  | [`magnetic_field/plot_magnetic_field.py`](examples/magnetic_field/plot_magnetic_field.py) | Peierls substitution; an Aharonov-Bohm ring's flux-periodic spectrum. |
@@ -217,6 +230,8 @@ thumbnailed example gallery under `docs/source/api/gallery/`.
217
230
  | [`topology/plot_edge_states.py`](examples/topology/plot_edge_states.py) | Zigzag graphene ribbon edge band; Kane-Mele helical edge states. |
218
231
  | [`dynamics/plot_bloch_oscillations.py`](examples/dynamics/plot_bloch_oscillations.py) | Wannier-Stark ladder, its localization, and Bloch oscillations under a uniform tilt. |
219
232
  | [`topology/plot_thouless_pump.py`](examples/topology/plot_thouless_pump.py) | The Rice-Mele model as a Thouless quantum pump: quantized Chern number and polarization winding. |
233
+ | [`strain/plot_pseudo_magnetic_field.py`](examples/strain/plot_pseudo_magnetic_field.py) | Triaxial strain as a gauge field: pseudo-Landau levels in graphene from a purely real Hamiltonian. |
234
+ | [`non_hermitian/plot_pt_symmetry.py`](examples/non_hermitian/plot_pt_symmetry.py) | PT symmetry, exceptional points, the Petermann factor, and a selectively amplified topological edge mode. |
220
235
 
221
236
  The `examples/` directory also has five older Jupyter notebooks (graphene
222
237
  flakes, kagome/Lieb/dumbbell lattices, disorder, strain, time propagation)
@@ -1,11 +1,12 @@
1
- # tbkit — a Tight-Binding package for research and education
1
+ # tbkit — a Tight-Binding package
2
2
 
3
3
  [![tests](https://github.com/cpoli/tbkit/actions/workflows/tests.yml/badge.svg)](https://github.com/cpoli/tbkit/actions/workflows/tests.yml)
4
+ [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://cpoli.github.io/tbkit/coverage/)
4
5
  [![docs](https://img.shields.io/badge/docs-cpoli.github.io%2Ftbkit-blue.svg)](https://cpoli.github.io/tbkit/)
5
6
  [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
6
7
  [![License: BSD 3-Clause](https://img.shields.io/badge/License-BSD--3--Clause-blue.svg)](LICENSE)
7
8
 
8
- ![tbkit logo](docs/source/_static/image/tbkit_logo.png)
9
+ ![tbkit logo](https://raw.githubusercontent.com/cpoli/tbkit/master/docs/source/_static/image/tbkit_logo.png)
9
10
 
10
11
  **tbkit** is a Python package to build and solve Tight-Binding models, written
11
12
  in vectorized NumPy/SciPy. It aims to make the mechanics of Tight-Binding
@@ -59,6 +60,13 @@ research prototyping.
59
60
 
60
61
  Requires Python >= 3.10.
61
62
 
63
+ ```bash
64
+ pip install tbkit
65
+ ```
66
+
67
+ or, for an editable install from a clone (e.g. to run the test suite or
68
+ work on tbkit itself):
69
+
62
70
  ```bash
63
71
  git clone https://github.com/cpoli/tbkit
64
72
  cd tbkit
@@ -72,6 +80,9 @@ pip install -e ".[test]"
72
80
  pytest tests/
73
81
  ```
74
82
 
83
+ The test suite has 100% line coverage of the `tbkit` package; see the
84
+ [HTML coverage report](https://cpoli.github.io/tbkit/coverage/).
85
+
75
86
  ## Quick start
76
87
 
77
88
  Real-space flake, nearest-neighbor square lattice:
@@ -178,6 +189,7 @@ thumbnailed example gallery under `docs/source/api/gallery/`.
178
189
 
179
190
  | Script | What it shows |
180
191
  |---------------------------------------------------------------------------|----------------|
192
+ | [`tight_binding/plot_square_lattice_bands.py`](examples/tight_binding/plot_square_lattice_bands.py) | The square lattice: Bloch's theorem at its simplest, bands along Gamma-X-M-Gamma, the nested Fermi surface, and the van Hove singularity. |
181
193
  | [`tight_binding/plot_graphene_bands.py`](examples/tight_binding/plot_graphene_bands.py) | Real-space flake + reciprocal-space band structure; graphene's Dirac point and Wallace's 1947 linear dispersion. |
182
194
  | [`tight_binding/plot_visualizing_a_model.py`](examples/tight_binding/plot_visualizing_a_model.py) | `tbkit.plot.Plot`: lattice, spectrum with sublattice polarization, density of states, eigenstate intensity. |
183
195
  | [`magnetic_field/plot_magnetic_field.py`](examples/magnetic_field/plot_magnetic_field.py) | Peierls substitution; an Aharonov-Bohm ring's flux-periodic spectrum. |
@@ -191,6 +203,8 @@ thumbnailed example gallery under `docs/source/api/gallery/`.
191
203
  | [`topology/plot_edge_states.py`](examples/topology/plot_edge_states.py) | Zigzag graphene ribbon edge band; Kane-Mele helical edge states. |
192
204
  | [`dynamics/plot_bloch_oscillations.py`](examples/dynamics/plot_bloch_oscillations.py) | Wannier-Stark ladder, its localization, and Bloch oscillations under a uniform tilt. |
193
205
  | [`topology/plot_thouless_pump.py`](examples/topology/plot_thouless_pump.py) | The Rice-Mele model as a Thouless quantum pump: quantized Chern number and polarization winding. |
206
+ | [`strain/plot_pseudo_magnetic_field.py`](examples/strain/plot_pseudo_magnetic_field.py) | Triaxial strain as a gauge field: pseudo-Landau levels in graphene from a purely real Hamiltonian. |
207
+ | [`non_hermitian/plot_pt_symmetry.py`](examples/non_hermitian/plot_pt_symmetry.py) | PT symmetry, exceptional points, the Petermann factor, and a selectively amplified topological edge mode. |
194
208
 
195
209
  The `examples/` directory also has five older Jupyter notebooks (graphene
196
210
  flakes, kagome/Lieb/dumbbell lattices, disorder, strain, time propagation)
@@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tbkit"
7
- version = "0.2.0"
8
- description = "A Python package to build and solve Tight-Binding models, for research and education."
7
+ version = "0.3.0"
8
+ description = "A Python package to build and solve Tight-Binding models."
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
11
11
  authors = [{ name = "Charles Poli", email = "cpoli374@gmail.com" }]
12
+ maintainers = [{ name = "cpoli", email = "cpoli374@gmail.com" }]
12
13
  requires-python = ">=3.10"
13
14
  dependencies = [
14
15
  "numpy>=1.24",
@@ -29,6 +30,13 @@ Homepage = "https://github.com/cpoli/tbkit"
29
30
  test = ["pytest>=7", "pytest-cov>=4"]
30
31
  docs = ["sphinx>=7", "pydata-sphinx-theme>=0.15", "sphinx-gallery>=0.15"]
31
32
 
33
+ [tool.pytest.ini_options]
34
+ # The suite is warning-free; keep it that way. A warning from tbkit itself is
35
+ # a defect (e.g. complex energies reaching matplotlib), and one from a test is
36
+ # usually the test misusing an API. Add a targeted `ignore::` entry below if a
37
+ # dependency ever emits something genuinely out of our control.
38
+ filterwarnings = ["error"]
39
+
32
40
  [tool.setuptools.packages.find]
33
41
  include = ["tbkit*"]
34
42
 
@@ -6,7 +6,7 @@
6
6
 
7
7
  """tbkit: build and solve Tight-Binding models."""
8
8
 
9
- __version__ = "0.2.0"
9
+ __version__ = "0.3.0"
10
10
 
11
11
  __all__ = [
12
12
  "Lattice", "System", "Plot", "Propagation", "Save", "KSpace",
@@ -47,8 +47,8 @@ def positive_int_lim(var, var_name, nmax):
47
47
  if var < 1:
48
48
  raise ValueError('\n\nParameter {} must be a positive integer.\n'.format(var_name))
49
49
  if var > nmax:
50
- raise ValueError('\n\nParameter {} must be a positive integer\n'\
51
- 'smaller than {}.\n'.format(var_name, nmax))
50
+ raise ValueError('\n\nParameter {} must be a positive integer '
51
+ 'smaller than {}.\n'.format(var_name, nmax))
52
52
 
53
53
 
54
54
  def real_number(var, var_name):
@@ -84,7 +84,7 @@ def positive_real_zero(var, var_name):
84
84
  if not isinstance(var, (int, float)):
85
85
  raise TypeError('\n\nParameter {} must be a real number.\n'.format(var_name))
86
86
  if var < 0:
87
- raise ValueError('\n\nParameter {} must be a positive number or zero.\n'.format(var_name))
87
+ raise ValueError('\n\nParameter {} must be a positive number or zero.\n'.format(var_name))
88
88
 
89
89
 
90
90
  def negative_real(var, var_name):
@@ -187,7 +187,7 @@ def ndarray_empty(var, var_name):
187
187
  :raises ValueError: Parameter *var* must not be an emptynumpy ndarray.
188
188
  '''
189
189
  if var.size == 0:
190
- raise ValueError('\n\nParameter {} must not be an emptynumpy ndarray.\n'.format(var_name))
190
+ raise ValueError('\n\nParameter {} must not be an empty numpy ndarray.\n'.format(var_name))
191
191
 
192
192
 
193
193
  def list_tuple_2elem(var, var_name):
@@ -200,7 +200,7 @@ def list_tuple_2elem(var, var_name):
200
200
  if var is None:
201
201
  return
202
202
  if not isinstance(var, (list, tuple)):
203
- raise TypeError('\n\nParameter {} must be a list/tuple\n'.format(var_name))
203
+ raise TypeError('\n\nParameter {} must be a list/tuple.\n'.format(var_name))
204
204
  if len(var) != 2:
205
205
  raise ValueError('\n\nParameter {} must be a list/tuple of length two.\n'.format(var_name))
206
206
 
@@ -215,7 +215,7 @@ def tuple_2elem(var, var_name):
215
215
  if var is None:
216
216
  return
217
217
  if not isinstance(var, tuple):
218
- raise TypeError('\n\nParameter {} must be a tuple\n'.format(var_name))
218
+ raise TypeError('\n\nParameter {} must be a tuple.\n'.format(var_name))
219
219
  if len(var) != 2:
220
220
  raise ValueError('\n\nParameter {} must be a list/tuple of length two.\n'.format(var_name))
221
221
 
@@ -284,18 +284,20 @@ def prim_vec(prim_vec):
284
284
  if not isinstance(prim_vec, list):
285
285
  raise TypeError('\n\nParameter prim_vec must be a list.\n')
286
286
  if not len(prim_vec) == 1 and not len(prim_vec) == 2:
287
- raise ValueError('\n\nParameter prim_vec must be a list.\n'
288
- 'of length 1 for 1D lattices or length 2 fro 2D lattices.\n')
287
+ raise ValueError('\n\nParameter prim_vec must be a list of length 1 '
288
+ 'for 1D lattices, or of length 2 for 2D lattices.\n')
289
289
  for coor in prim_vec:
290
290
  if not isinstance(coor, tuple):
291
- raise TypeError('\n\nParameter prim_vec contain tuples\n')
291
+ raise TypeError('\n\nParameter prim_vec must contain tuples.\n')
292
292
  if len(coor) != 2:
293
- raise ValueError('\n\nParameter prim_vec contain tuples of length 2.\n')
293
+ raise ValueError('\n\nParameter prim_vec must contain tuples of length 2.\n')
294
294
  if (not isinstance(coor[0], (int, float))) or \
295
295
  (not isinstance(coor[1], (int, float))):
296
- raise ValueError('\n\ncoor must contain real numbers.\n')
296
+ raise ValueError('\n\nParameter prim_vec must contain tuples of '
297
+ 'real numbers.\n')
297
298
  if coor[0] ** 2 + coor[1] ** 2 < 0.1:
298
- raise ValueError('\n\nNorm of coor should be larger than 0.1.\n')
299
+ raise ValueError('\n\nEach primitive vector must have a norm larger '
300
+ 'than 0.1.\n')
299
301
 
300
302
 
301
303
  def get_lattice(prim_vec, n1, n2):
@@ -374,22 +376,6 @@ def boundary_line(cx, cy, co):
374
376
  raise TypeError('\n\nParameter co must be a real number.\n')
375
377
 
376
378
 
377
- def ellipse(a, b):
378
- '''
379
- Check *ellipse_in* and *ellipse_out*.
380
- :raises TypeError: Parameter a must be a positive number.
381
- :raises TypeError: Parameter b must be a positive number.
382
- '''
383
- if not isinstance(a, (int, float)):
384
- raise TypeError('\n\nParameter a must be a positive number.\n')
385
- if not isinstance(b, (int, float)):
386
- raise TypeError('\n\nParameter b must be a positive number.\n')
387
- if a <= 0:
388
- raise ValueError('\n\nParameter a must be a positive number.\n')
389
- if b <= 0:
390
- raise ValueError('\n\nParameter b must be a positive number.\n')
391
-
392
-
393
379
  def sites(sites):
394
380
  '''
395
381
  Check if *get_lattice* has been called (*coor* not empty).
@@ -471,7 +457,7 @@ def set_hopping(list_hop, n_max):
471
457
  if not isinstance(dic['n'], int):
472
458
  raise TypeError('\n\n"n" value must be an integer.\n')
473
459
  if not 0 < dic['n'] <= n_max:
474
- raise ValueError('\n\n"n" value must be between 1 and nmax".\n')
460
+ raise ValueError('\n\n"n" value must be between 1 and {}.\n'.format(n_max))
475
461
  if not isinstance(dic['t'], (int, float, complex)):
476
462
  raise TypeError('\n\n"t" value must be a real or complex number.\n')
477
463
  if len(dic) == 3:
@@ -857,7 +843,8 @@ def set_hopping_kspace(list_hop, n_sites, ndim, spin=False):
857
843
  if not all(isinstance(n, int) for n in dic['R']):
858
844
  raise TypeError('\n\n"R" must be a tuple of integers.\n')
859
845
  if dic['i'] == dic['j'] and dic['R'] == (0,) * ndim:
860
- raise ValueError('\n\nUse kspace.set_onsite for i == j and R == 0.\n')
846
+ raise ValueError('\n\nUse kspace.set_onsite for i == j and R == 0 '
847
+ '(it accepts a 2x2 spin matrix when spin=True).\n')
861
848
  if spin:
862
849
  spin_matrix(dic['t'], '"t"')
863
850
  elif not isinstance(dic['t'], (int, float, complex)):
@@ -871,7 +858,7 @@ def set_onsite_kspace(dict_onsite, tags, spin=False):
871
858
  :raises TypeError: Parameter *dict_onsite* must be a dictionary.
872
859
  :raises ValueError: keys must be tags.
873
860
  :raises TypeError: values must be real or complex numbers (or, if
874
- *spin*, a pair of real/complex numbers).
861
+ *spin*, a pair of real/complex numbers, or a 2x2 matrix).
875
862
  '''
876
863
  if not isinstance(dict_onsite, dict):
877
864
  raise TypeError('\n\nParameter dict_onsite must be a dictionary.\n')
@@ -879,10 +866,13 @@ def set_onsite_kspace(dict_onsite, tags, spin=False):
879
866
  if tag not in tags:
880
867
  raise ValueError('\n\nParameter dict_onsite keys must be a tag.\n')
881
868
  if spin and not isinstance(val, (int, float, complex)):
882
- if not (isinstance(val, (tuple, list)) and len(val) == 2
883
- and all(isinstance(v, (int, float, complex)) for v in val)):
869
+ is_pair = (isinstance(val, (tuple, list)) and len(val) == 2
870
+ and all(isinstance(v, (int, float, complex)) for v in val))
871
+ is_mat = np.ndim(val) == 2 and np.shape(val) == (2, 2)
872
+ if not (is_pair or is_mat):
884
873
  raise TypeError('\n\nParameter dict_onsite values must be a number, or, '
885
- 'if spin, a pair of numbers (E_up, E_down).\n')
874
+ 'if spin, a pair of numbers (E_up, E_down) or a '
875
+ '2x2 matrix.\n')
886
876
  elif not spin and not isinstance(val, (int, float, complex)):
887
877
  raise TypeError('\n\nParameter dict_onsite values must be real and/or complex numbers.\n')
888
878
 
@@ -106,40 +106,74 @@ class GrapheneSystem(System):
106
106
  def __init__(self, lat: Lattice) -> None:
107
107
  System.__init__(self, lat)
108
108
 
109
- def set_hop_linear_strain(self, t: complex, beta: float) -> None:
109
+ def _strain_projection(self) -> tuple[NDArray[np.int64], NDArray[np.float64], NDArray[np.float64]]:
110
+ r'''
111
+ Private method.
112
+
113
+ Get, for every nearest-neighbor bond, the projection
114
+
115
+ .. math::
116
+
117
+ s_{ij} = \hat{\boldsymbol\delta}_{ij}\cdot\mathbf{r}_{ij}
118
+
119
+ of its midpoint :math:`\mathbf{r}_{ij}` on its (outward-oriented)
120
+ direction :math:`\hat{\boldsymbol\delta}_{ij}`, so that the strained
121
+ hopping is :math:`t_{ij} = t(1 + \tfrac14\beta s_{ij})`.
122
+
123
+ :returns:
124
+ * **ind_up** -- Integer ndarray, shape (nbonds, 2). Bond indices.
125
+ * **ang** -- Real ndarray. Bond angles, in degrees.
126
+ * **s** -- Real ndarray. The projections above.
110
127
  '''
111
- Set nearest neighbors hoppings according to the linear trixial strain.
112
-
128
+ self.get_distances()
129
+ ind = np.argwhere(np.isclose(self.dist_uni[1], self.vec_hop['dis'], atol=ATOL))
130
+ ind_up = ind[ind[:, 1] > ind[:, 0]]
131
+ ang = self.vec_hop['ang'][ind_up[:, 0], ind_up[:, 1]].copy()
132
+ # orient the three bond families consistently outwards
133
+ ang[np.isclose(30., ang, atol=ATOL)] = -150.
134
+ ang[np.isclose(150., ang, atol=ATOL)] = -30.
135
+ x_center = .5 * (self.lat.coor['x'][ind_up[:, 0]] + self.lat.coor['x'][ind_up[:, 1]])
136
+ y_center = .5 * (self.lat.coor['y'][ind_up[:, 0]] + self.lat.coor['y'][ind_up[:, 1]])
137
+ s = (np.cos(PI / 180 * ang) * x_center + np.sin(PI / 180 * ang) * y_center)
138
+ return ind_up, ang, s
139
+
140
+ def set_hop_linear_strain(self, t: complex, beta: float) -> None:
141
+ r'''
142
+ Set nearest-neighbor hoppings according to a linear triaxial strain:
143
+
144
+ .. math::
145
+
146
+ t_{ij} = t\left(1 + \tfrac14\beta\,
147
+ \hat{\boldsymbol\delta}_{ij}\cdot\mathbf{r}_{ij}\right)
148
+
149
+ with :math:`\hat{\boldsymbol\delta}_{ij}` the bond direction and
150
+ :math:`\mathbf{r}_{ij}` its midpoint. The strain is measured from the
151
+ coordinate origin, so centre the flake on it (see *lattice.center*)
152
+ before calling this.
153
+
113
154
  :param t: Hopping value without strain.
114
- :param beta: Strength of the strain.
155
+ :param beta: Strength of the strain. See *get_beta_lims* for the
156
+ range that keeps every hopping positive.
115
157
  '''
116
158
  error_handling.number(t, 't')
117
159
  error_handling.real_number(beta, 'beta')
118
- self.get_distances()
119
- ind = np.argwhere(np.isclose(self.dist_uni[1], self.vec_hop['dis'], atol=ATOL))
120
- ind_up = ind[ind[:, 1] > ind[:, 0]]
160
+ ind_up, ang, s = self._strain_projection()
121
161
  self.hop = np.zeros(len(ind_up), dtype=HOP_DTYPE)
122
162
  self.hop['n'] = 1
123
163
  self.hop['i'] = ind_up[:, 0]
124
164
  self.hop['j'] = ind_up[:, 1]
125
165
  self.hop['ang'] = self.vec_hop['ang'][ind_up[:, 0], ind_up[:, 1]]
126
- # change angle (to get the correct strain)
127
- self.hop['ang'][np.isclose(30., self.hop['ang'], ATOL)] = -150.
128
- self.hop['ang'][np.isclose(150., self.hop['ang'], ATOL)] = - 30.
129
- x_center = .5 * (self.lat.coor['x'][ind_up[:, 0]] + self.lat.coor['x'][ind_up[:, 1]])
130
- y_center = .5 * (self.lat.coor['y'][ind_up[:, 0]] + self.lat.coor['y'][ind_up[:, 1]])
131
- self.hop['t'] = t * (1. + 0.25 * beta * (np.cos(PI / 180 * self.hop['ang']) * x_center +
132
- np.sin(PI / 180 * self.hop['ang']) * y_center))
133
- # back to the former angle
134
- self.hop['ang'][np.isclose(-150., self.hop['ang'])] = 30.
135
- self.hop['ang'][np.isclose(-30., self.hop['ang'])] = 150.
166
+ self.hop['tag'] = npc.add(self.lat.coor['tag'][ind_up[:, 0]],
167
+ self.lat.coor['tag'][ind_up[:, 1]])
168
+ self.hop['t'] = t * (1. + 0.25 * beta * s)
136
169
 
137
170
  def get_butterfly(self, t: complex, N: int) -> None:
138
- ''''
171
+ '''
139
172
  Get energies depending on strain.
140
173
 
141
174
  :param t: Unstrained hopping value.
142
- :param N: number of strain values between min and max strains.
175
+ :param N: Positive integer. Number of strain values between the
176
+ minimal and maximal strains given by *get_beta_lims*.
143
177
  '''
144
178
  error_handling.number(t, 't')
145
179
  error_handling.positive_int(N, 'N')
@@ -147,25 +181,29 @@ class GrapheneSystem(System):
147
181
  self.betas = np.linspace(beta_lims[0], beta_lims[1], N)
148
182
  self.butterfly = np.zeros((N, self.lat.sites))
149
183
  for i, beta in enumerate(self.betas):
150
- self.set_hop_linear_strain(t=1, beta=beta)
184
+ self.set_hop_linear_strain(t=t, beta=beta)
151
185
  self.get_ham()
152
186
  self.butterfly[i] = LA.eigvalsh(self.ham.toarray())
153
187
 
154
188
  def get_beta_lims(self) -> NDArray[np.float64]:
155
- '''
156
- Get the extremal values of strain keeping positive hoppings.
157
- '''
158
- beta_lims = np.zeros(2)
159
- yb_min_val = self.lat.coor['y'][self.lat.coor['tag'] == 'b'].min()
160
- yb_min = self.lat.coor['y'][self.lat.coor['y'] == yb_min_val][0]
161
- ym = 0.5 * (2 * yb_min + 1)
162
- beta_lims[1] = -4. / ym + 1e-6
163
- yb_max_val = self.lat.coor['y'][self.lat.coor['tag'] == 'a'].max()
164
- yb_max = self.lat.coor['y'][self.lat.coor['y'] == yb_max_val][0]
165
- ym = 0.5 * (2 * yb_max - 1)
166
- beta_lims[0] = -4. / ym + 1e-6
167
- print('Strain limits: {}'.format(beta_lims))
168
- return beta_lims
189
+ r'''
190
+ Get the extremal strain values keeping every hopping positive.
191
+
192
+ Under *set_hop_linear_strain* a bond's amplitude is
193
+ :math:`t(1+\tfrac14\beta s_{ij})`, so it stays positive for every bond
194
+ iff :math:`-4/\max_{ij} s_{ij} < \beta < -4/\min_{ij} s_{ij}`.
195
+
196
+ :returns:
197
+ * **beta_lims** -- Real ndarray of length 2, ``[beta_min, beta_max]``
198
+ (ascending). A bound is infinite if the corresponding
199
+ :math:`s_{ij}` never takes that sign.
200
+ '''
201
+ _, _, s = self._strain_projection()
202
+ eps = 1e-6
203
+ s_min, s_max = s.min(), s.max()
204
+ beta_min = -4. / s_max + eps if s_max > 0 else -np.inf
205
+ beta_max = -4. / s_min - eps if s_min < 0 else np.inf
206
+ return np.array([beta_min, beta_max])
169
207
 
170
208
 
171
209
  # Backward-compatible camelCase aliases (pre-0.2 API).
@@ -97,6 +97,9 @@ class KSpace():
97
97
  self.norb = 2*self.n_sites if spin else self.n_sites
98
98
  self.tags = np.array([dic['tag'] for dic in lat.unit_cell])
99
99
  self.onsite = np.zeros(self.norb, 'c16')
100
+ # spin-off-diagonal onsite terms (in-plane Zeeman, onsite Rashba):
101
+ # they have no place on the diagonal `onsite` array.
102
+ self._onsite_offdiag = np.zeros((self.norb, self.norb), 'c16')
100
103
  self._hop = [] # list of (i, j, R_cartesian (np.ndarray), t)
101
104
  self.rec_vec = reciprocal_vectors(lat.prim_vec)
102
105
  self.ks = np.array([]) # k-points of the last band-structure calculation
@@ -109,24 +112,35 @@ class KSpace():
109
112
  Set the onsite energies, by sublattice tag.
110
113
 
111
114
  :param dict_onsite: Dictionary. key: tag, val: onsite energy
112
- (a plain number), or, if ``spin=True``, either a plain number
113
- (applied equally to both spins) or a pair ``(E_up, E_down)`` of
114
- numbers (a spin splitting, e.g. a Zeeman term along z).
115
+ (a plain number), or, if ``spin=True``, a plain number (applied
116
+ equally to both spins), a pair ``(E_up, E_down)`` of numbers (a
117
+ spin splitting along z), or a 2x2 complex matrix (a general spin
118
+ structure, e.g. an in-plane Zeeman field built from :data:`PAULI`).
115
119
 
116
120
  Example usage::
117
121
 
118
122
  kag.set_onsite({'a': 1., 'b': -1.})
119
123
  # spinful: same onsite energy for both spins on 'a', a Zeeman
120
- # splitting on 'b':
124
+ # splitting along z on 'b':
121
125
  kag_spin.set_onsite({'a': 1., 'b': (1., -1.)})
126
+ # spinful: an in-plane Zeeman field on 'a':
127
+ kag_spin.set_onsite({'a': Bx*PAULI['x']})
122
128
  '''
123
129
  error_handling.set_onsite_kspace(dict_onsite, self.lat.tags, self.spin)
124
130
  for tag, val in dict_onsite.items():
125
131
  sites = np.where(self.tags == tag)[0]
126
132
  if self.spin:
127
- e_up, e_down = (val, val) if isinstance(val, (int, float, complex)) else val
128
- self.onsite[2*sites] = e_up
129
- self.onsite[2*sites + 1] = e_down
133
+ if isinstance(val, (int, float, complex)):
134
+ block = val * PAULI['0']
135
+ elif np.ndim(val) == 2:
136
+ block = np.asarray(val, 'c16')
137
+ else:
138
+ block = np.diag(np.asarray(val, 'c16'))
139
+ self.onsite[2*sites] = block[0, 0]
140
+ self.onsite[2*sites + 1] = block[1, 1]
141
+ for site in sites:
142
+ self._onsite_offdiag[2*site, 2*site + 1] = block[0, 1]
143
+ self._onsite_offdiag[2*site + 1, 2*site] = block[1, 0]
130
144
  else:
131
145
  self.onsite[sites] = val
132
146
 
@@ -187,16 +201,23 @@ class KSpace():
187
201
  r'''
188
202
  Get the dense Bloch Hamiltonian :math:`H(\mathbf{k})`.
189
203
 
190
- :param k: Tuple/list/ndarray of one/two real numbers. :math:`\mathbf{k}` point,
191
- in the same Cartesian frame as *prim_vec*.
204
+ :param k: Tuple/list/ndarray of one/two real numbers. In 2D, the
205
+ :math:`\mathbf{k}` point in the same Cartesian frame as *prim_vec*.
206
+ In 1D, the crystal momentum *along* the primitive vector (so the
207
+ Brillouin zone spans :math:`2\pi/|\mathbf{a}_1|`), which
208
+ coincides with :math:`k_x` for a chain aligned with :math:`x`.
192
209
 
193
210
  :returns:
194
211
  * **ham** -- Complex ndarray, shape (norb, norb).
195
212
  '''
196
213
  error_handling.k_vector(k, 'k', self.dim)
197
214
  k_cart = np.zeros(2)
198
- k_cart[:self.dim] = k
199
- ham = np.diag(self.onsite).astype('c16')
215
+ if self.dim == 1:
216
+ a1 = np.asarray(self.lat.prim_vec[0], dtype='f8')
217
+ k_cart = np.ravel(np.asarray(k, dtype='f8'))[0] * a1 / np.linalg.norm(a1)
218
+ else:
219
+ k_cart[:self.dim] = k
220
+ ham = np.diag(self.onsite).astype('c16') + self._onsite_offdiag
200
221
  for i, j, R_cart, t in self._hop:
201
222
  ham[i, j] += t * np.exp(1j * np.dot(k_cart, R_cart))
202
223
  return ham
@@ -283,7 +304,9 @@ class KSpace():
283
304
  rec_vec = [np.array(b) for b in self.rec_vec]
284
305
  if self.dim == 1:
285
306
  f1 = np.arange(nk[0]) / nk[0]
286
- ks = f1[:, None] * rec_vec[0][None, :self.dim]
307
+ # k is the crystal momentum along the chain (see get_ham), so the
308
+ # zone spans |b1| = 2*pi/|a1|.
309
+ ks = f1[:, None] * np.linalg.norm(rec_vec[0])
287
310
  return [f1], ks
288
311
  f1, f2 = np.meshgrid(np.arange(nk[0])/nk[0], np.arange(nk[1])/nk[1], indexing='ij')
289
312
  ks = (f1.ravel()[:, None] * rec_vec[0][None, :]
@@ -177,7 +177,7 @@ class Lattice():
177
177
 
178
178
  def shift_y(self, shift: float) -> None:
179
179
  '''
180
- Shift by *delta_x* the x coordinates.
180
+ Shift the y coordinates.
181
181
 
182
182
  :param shift: Real number. Shift value.
183
183
  '''
@@ -222,7 +222,6 @@ class Lattice():
222
222
 
223
223
  (x-x_0)^2/a^2+(y-y_0)^2/b^2 < 1\, .
224
224
 
225
- :param list_hop: List of Dictionary (see set_hopping definition).
226
225
  :param rx: Positive Real number. Radius along :math:`x`.
227
226
  :param ry: Positive Real number. Radius along :math:`y`.
228
227
  :param x0: Real number. :math:`x` center.
@@ -246,7 +245,6 @@ class Lattice():
246
245
  (x-x_0)^2/a^2+(y-y_0)^2/b^2 > 1\, .
247
246
 
248
247
 
249
- :param list_hop: List of Dictionary (see set_hopping definition).
250
248
  :param rx: Positive Real number. Radius along :math:`x`.
251
249
  :param ry: Positive Real number. Radius along :math:`y`.
252
250
  :param x0: Real number. :math:`x` center.
@@ -271,18 +269,18 @@ class Lattice():
271
269
 
272
270
  def rotation(self, theta: float) -> None:
273
271
  r'''
274
- Rotate the lattice structure by the angle :math:`\theta`.
272
+ Rotate the lattice structure about the origin by the angle
273
+ :math:`\theta`.
275
274
 
276
275
  :param theta: Rotation angle in degrees.
277
276
  '''
278
277
  error_handling.empty_coor(self.coor)
279
278
  error_handling.real_number(theta, 'theta')
280
- theta *= PI / 360
281
- for dic in self.unit_cell:
282
- x = self.coor['x'] - dic['r0'][0]
283
- y = self.coor['y'] - dic['r0'][1]
284
- self.coor['x'] = x * np.cos(theta) - y * np.sin(theta) + dic['r0'][0]
285
- self.coor['y'] = y * np.cos(theta) + x* np.sin(theta) + dic['r0'][1]
279
+ theta *= PI / 180
280
+ x = self.coor['x'].copy()
281
+ y = self.coor['y'].copy()
282
+ self.coor['x'] = x * np.cos(theta) - y * np.sin(theta)
283
+ self.coor['y'] = y * np.cos(theta) + x * np.sin(theta)
286
284
 
287
285
  def clean_coor(self) -> None:
288
286
  '''
@@ -370,10 +368,10 @@ class Lattice():
370
368
  figsize: tuple[float, float] | None = None,
371
369
  ) -> Figure:
372
370
  '''
373
- Plot lattice in hopping space.
371
+ Plot the lattice in real space.
374
372
 
375
373
  :param ms: Positive number. Default value 20. Markersize.
376
- :param fs: Positve number. Default value 20. Fontsize.
374
+ :param fs: Positive number. Default value 20. Fontsize.
377
375
  :param plt_index: Boolean. Default value False. Plot site labels.
378
376
  :param axis: Boolean. Default value False. Plot axis.
379
377
  :param figsize: Tuple. Default value None. Figsize.